// ==UserScript==
// @name [RED] Cover Inspector
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.15.13
// @run-at document-end
// @description Easify & speed-up finding, lookup and updating of invalid, missing or non optimal album covers on site
// @author Anakunda
// @copyright 2024, 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
// @match https://redacted.ch/top10.php
// @match https://redacted.ch/top10.php?*
// @match https://redacted.ch/better.php?method=*
// @match https://redacted.ch/better.php?page=*&method=*
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getTabs
// @grant GM_saveTab
// @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// ==/UserScript==
{
'use strict';
const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
const httpParser = /^(https?:\/\/\S+)$/i;
const preferredHosts = {
'redacted.ch': ['ptpimg.me'],
}[document.domain];
const preferredTypes = GM_getValue('preferred_types', ['jpeg', 'png', 'gif', 'jpg'].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) {
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
const e = (size = Math.max(size, 0)) > 0 ? Math.min(Math.floor(Math.log2(size) / 10), units.length - 1) : 0;
return `${(size / Math.pow(2, e * 10)).toFixed(Math.min(e, 3))}\xA0${units[e]}`;
}
const imageHostHelper = 'imageHostHelper' in unsafeWindow ? unsafeWindow.imageHostHelper ? Promise.resolve(unsafeWindow.imageHostHelper)
: Promise.reject('Assertion failed: void unsafeWindow.imageHostHelper') : new Promise(function(resolve, reject) {
function listener(evt) {
clearTimeout(timeout);
unsafeWindow.removeEventListener('imageHostHelper', listener);
//console.log('imageHostHelper exports triggered:', evt);
if (evt.data) resolve(evt.data); else if (unsafeWindow.imageHostHelper) resolve(unsafeWindow.imageHostHelper);
else reject('Assertion failed: void unsafeWindow.imageHostHelper');
}
unsafeWindow.addEventListener('imageHostHelper', listener);
const timeout = setTimeout(function() {
unsafeWindow.removeEventListener('imageHostHelper', listener);
reject('Timeout reached');
}, 15000);
});
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 openTabParams = { }, tabData = { torrentGroups: { } };
if (GM_getValue('view_group_with_google_search', true)) openTabParams['embed-google-image-search'] = 1;
if (GM_getValue('view_group_with_cse_search', false)) openTabParams['embed-cse-search'] = 1;
if (GM_getValue('view_group_with_desc_source', false)) openTabParams['embed-desc-link-source'] = 1;
if (GM_getValue('view_group_with_desc_images', true)) openTabParams['desc-links-image-preview'] = 1;
if (GM_getValue('view_group_with_collages_highlighting', true)) openTabParams['highlight-cover-collages'] = 1;
if (GM_getValue('view_group_presearch_bandcamp', true)) openTabParams['presearch-bandcamp'] = 1;
function openGroup(torrentGroup) {
if (!torrentGroup) throw 'Invalid argument';
if (!(torrentGroup.group.id > 0)) return null;
tabData.torrentGroups[torrentGroup.group.id] = torrentGroup;
GM_saveTab(tabData);
return openTabLimited('torrents', Object.assign({ id: torrentGroup.group.id }, openTabParams));
}
function checkSavedRecovery() {
if ('coverInspectorTabsQueue' in localStorage) try {
const savedQueue = JSON.parse(localStorage.getItem('coverInspectorTabsQueue'));
if (!Array.isArray(savedQueue) || savedQueue.length <= 0) return true;
const unloadedCount = savedQueue.filter(item1 => !tabsQueueRecovery.some(function(item2) {
if (item1.endpoint != item2.endpoint || item1.hash != item2.hash) return false;
if ((item1.params == null) != (item2.params == null)) return false;
return item1.params == null || Object.keys(item1.params).every(key => item2[key] == item1[key])
&& Object.keys(item2.params).every(key => item1[key] == item2[key]);
})).length;
if (unloadedCount <= 0) return true;
return confirm('Saved queue (' + (unloadedCount < savedQueue.length ? unloadedCount + '/' + savedQueue.length
: savedQueue.length) + ' tabs to open) will be lost, continue?');
}catch(e) { console.warn(e) }
return true;
}
const acceptableSize = { 'redacted.ch': GM_getValue('acceptable_cover_size') }[document.domain] || 4 * 2**10;
const fineResolution = { 'redacted.ch': 500 }[document.domain] || 500;
let acceptableResolution = { 'redacted.ch': GM_getValue('acceptable_cover_resolution') }[document.domain] || 300;
if (fineResolution > 0 && acceptableResolution > fineResolution) acceptableResolution = fineResolution;
let hqResolution = { 'redacted.ch': GM_getValue('hq_cover_resolution') }[document.domain] || 1024;
if (fineResolution > 0 && hqResolution < fineResolution) hqResolution = 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'],
'AnimeBytes': ['animebytes.tv'],
'Apple': ['mzstatic.com'],
'Archive': ['archive.org'],
'Bandcamp': ['bcbits.com'],
'Bangumi': ['bgm.tv'],
'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', 'discordapp.com'],
'e-onkyo': ['e-onkyo.com'],
'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'],
'Naxos': ['cdn.naxos.com'],
'NetEase': ['126.net'],
'NoelShack': ['noelshack.com'],
'OTOTOY': ['ototoy.jp'],
'Photobucket': ['photobucket.com'],
'PicaBox': ['picabox.ru'],
'PicLoad': ['free-picload.com', 'picload.org'],
'PimpAndHost': ['pimpandhost.com'],
'Pinterest': ['pinimg.com'],
'PixHost': ['pixhost.to'],
'PomfCat': ['pomf.cat'],
'PostImg': ['postimg.cc'],
'ProgArchives': ['progarchives.com'],
'PTPimg': ['ptpimg.me'],
'Qobuz': ['qobuz.com'],
'QQ音乐': ['qq.com'],
'Ra': ['thesungod.xyz'],
'Radikal': ['radikal.ru'],
'RA': ['residentadvisor.net'],
'RYM': ['e.snmc.io'],
'SavePhoto': ['savephoto.ru'],
'Shopify': ['shopify.com'],
'Slowpoke': ['slow.pics'],
'SoundCloud': ['sndcdn.com'],
'Spotify': ['scdn.co'],
'Stereogum': ['stereogum.com'],
'SM.MS': ['sm.ms', 'loli.net'],
'Stereogum': ['stereogum.com'],
'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(domain =>
imageUrl == (domain = domain.toLowerCase()) || 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', 'dicmusic.com', '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', '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', 'rateyourmusic.com', 'e.snmc.io',
'thewire.co.uk', 'bgm.tv', '126.net', 'e-onkyo.com', 'stereogum.com', 'scdn.co', 'qq.com',
];
const click2goHostLists = [
GM_getValue('click2go_blacklist', ['amazonaws.com', 'coverartarchive.org', 'archive.org']),
GM_getValue('click2go_whitelist', musicResourceDomains.concat([
'imgur.com', 'forumbilder.com', 'jpopsuki.eu', 'pinimg.com', 'shopify.com',
'twimg.com', 'discordapp.com', 'discordapp.net',
])),
GM_getValue('click2go_badlist', [
'photobucket.com', 'imgs.onl', 'sli.mg', 'picload.org', 'kprofiles.com',
]),
].map(click2goHostList => click2goHostList.filter((e, n, a) => a.indexOf(e) == n).sort());
if (click2goHostLists[0].some(hostName => /\b(?:imgur\.com)$/i.test(hostName))) GM_setValue('click2go_blacklist',
click2goHostLists[0] = click2goHostLists[0].filter(hostName => !/\b(?:imgur\.com)$/i.test(hostName)));
if (!click2goHostLists[1].includes('imgur.com')) {
click2goHostLists[1].push('imgur.com');
GM_setValue('click2go_whitelist', click2goHostLists[1].sort());
}
const getDomainListIndex = (domain, listNdx) => domain && Array.isArray(listNdx = click2goHostLists[listNdx]) ?
(domain = domain.toLowerCase(), listNdx.findIndex(domain2 => (domain2 = domain2.toLowerCase()) == domain
|| domain.endsWith('.' + domain2))) : -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'; else if (!hasArtworkSet(img)) return '';
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 = { };
const imgLoadCache = new Map, imgRequestCache = new Map;
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([
(function(imageUrl) {
if (imgLoadCache.has(imageUrl)) return imgLoadCache.get(imageUrl);
const loadWorker = new Promise(function(resolve, reject) {
const image = new Image;
image.onload = evt => { resolve({
src: evt.currentTarget.src,
width: evt.currentTarget.naturalWidth,
height: evt.currentTarget.naturalHeight,
}) };
image.onerror = evt => { reject(evt.message || 'Image loading error (' + image.src + ')') };
image.loading = 'eager';
image.referrerPolicy = 'same-origin';
image.src = imageUrl;
});
imgLoadCache.set(imageUrl, loadWorker);
return loadWorker;
})(imageUrl),
(function(imageUrl) {
if (imgRequestCache.has(imageUrl)) return imgRequestCache.get(imageUrl);
const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) {
const params = { method: method, url: imageUrl, binary: true, timeout: 90e3, responseType: 'blob' };
setUserAgent(params);
let hdrSize, hdrType, hXHR = GM_xmlhttpRequest(Object.assign(params, {
onreadystatechange: function(response) {
if (hdrSize >= 0 && type || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
if (response.status < 200 || response.status >= 400) {
reject(defaultErrorHandler(response));
return hXHR.abort();
}
if ([
'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',
'/images/no-cover.png',
].some(invalidUrl => response.finalUrl.endsWith(invalidUrl))) {
reject('Dummy image (placeholder): ' + response.finalUrl);
return hXHR.abort();
}
const Etag = /^(?:Etag)\s*:\s*(.+?)\s*$/im.exec(response.responseHeaders);
if (Etag != null && [
'd835884373f4d6c8f24742ceabe74946',
'25d628d3d3a546cc025b3685715e065f42f9cbb735688b773069e82aac16c597f03617314f78375d143876b6d8421542109f86ccd02eab6ba8b0e469b67dc953',
'"55fade2068e7503eae8d7ddf5eb6bd09"',
'"1580238364"',
'"rbFK6Ned4SXbK7Fsn+EfdgKVO8HjvrmlciYi8ZvC9Mc"',
'7ef77ea97052c1abcabeb44ad1d0c4fce4d269b8a4f439ef11050681a789a1814fc7085a96d23212af594b6b2855c99f475b8b61d790f22b9d71490425899efa',
].some(etag => etag.toLowerCase() == Etag[1].toLowerCase())) {
reject('Dummy image (placeholder): ' + response.finalUrl);
return hXHR.abort();
}
hdrSize = /^(?:Content-Length)\s*:\s*(\d+)\b/im.exec(response.responseHeaders);
hdrSize = hdrSize != null ? parseInt(hdrSize[1]) : undefined;
hdrType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders);
hdrType = hdrType != null ? hdrType[1] : undefined;
if (hdrSize >= 0 && hdrType) {
resolve({ size: hdrSize, type: hdrType });
if (method != 'HEAD') hXHR.abort();
} else if (method == 'HEAD') reject('Content size/type missing or invalid in header');
},
onload: function(response) {
if (response.status >= 200 && response.status < 400) try {
if ((hdrSize = response.response) instanceof Blob) resolve({
size: hdrSize.size,
type: hdrSize.type || hdrType,
}); else if ((hdrSize = new Blob([response.responseText])) instanceof Blob) resolve({
size: hdrSize.size,
type: hdrType,
}); else reject('Image content could not be loaded');
} catch(e) { reject(e) } else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}));
});
const loadWorker = getByXHR('GET'); //getByXHR('HEAD').catch(reason => getByXHR('GET'));
imgRequestCache.set(imageUrl, loadWorker);
return loadWorker;
})(imageUrl).catch(function(reason) {
console.warn(`[Cover Inspector] Failed to request remote image (${imageUrl}):`, reason);
return null;
}),
]).then(workers => Object.assign({ localProxy: false }, workers[0], workers[1])).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');
let noEditPerms = document.getElementById('nav_userclass'), cseSearchMenu;
noEditPerms = noEditPerms != null && ['User', 'Member', 'Power User'].includes(noEditPerms.textContent.trim());
const [readOnly, noBatchProcessing] = ['read_only', 'no_batch_processing'].map(prefName => GM_getValue(prefName, false));
const noAutoLookups = GM_getValue('no_auto_lookups', true);
const coverRelatedCollages = {
'redacted.ch': {
invalid: [31445],
poor: [33309, 33307, 33308, 33310, 33314, 31735, 33306, 33311, 33312, 33313],
investigate: [20036],
missing: undefined,
},
}[document.domain];
const inCoversCollage = (collageIndex, torrentGroup) => coverRelatedCollages
&& Array.isArray(coverRelatedCollages[collageIndex]) && coverRelatedCollages[collageIndex][0] > 0
&& torrentGroup && Array.isArray(torrentGroup.group.collages)
&& torrentGroup.group.collages.some(collage => coverRelatedCollages[collageIndex].includes(collage.id));
function addToCoversCollage(collageIndex, groupId) {
if (!coverRelatedCollages || !(Array.isArray(coverRelatedCollages[collageIndex]))
|| coverRelatedCollages[collageIndex].length <= 0)
return Promise.reject('Cover related collage not defined for current site');
if (!(groupId > 0)) throw 'Invalid argument';
return ajaxApiKey ? queryAjaxAPI('addtocollage', {
collageid: coverRelatedCollages[collageIndex][GM_getValue('indifferent_collages', true) ?
Math.floor(Math.random() * coverRelatedCollages[collageIndex].length) : 0],
}, { 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 (this.readyState < XMLHttpRequest.DONE) return;
if (this.status >= 200 && this.status < 400) resolve(this.status); else reject(defaultErrorHandler(this));
};
xhr.onerror = function() { reject(defaultErrorHandler(this)) };
xhr.ontimeout = function() { reject(defaultTimeoutHandler(this)) };
xhr.send(payLoad);
}) : Promise.reject('Not supported on this page');
}
const removeFromCoversCollage = (collageIndex, torrentGroup) => coverRelatedCollages
&& Array.isArray(coverRelatedCollages[collageIndex]) ? Promise.all(torrentGroup.group.collages
.map(collage => collage.id).filter(collageId => coverRelatedCollages[collageIndex].includes(collageId))
.map(collageId => removeFromCollage(collageId, torrentGroup.group.id))).then(statuses => statuses.length)
: Promise.reject('Cover related collages not defined for current site');
const testImageQuality = (imageUrl, acceptableLevel = 0) => getImageDetails(imageUrl).then(function(imageDetails) {
const loDim = Math.min(imageDetails.width, imageDetails.height);
const level = loDim < acceptableResolution ? 0 : loDim < fineResolution ? 1 : loDim < hqResolution ? 2 : 3;
return level < acceptableLevel ? Promise.reject('Poor image resolution') : level;
});
function getLinks(descBody) {
if (!descBody) return null;
if (typeof descBody == 'string') descBody = domParser.parseFromString(descBody, 'text/html').body;
if (descBody instanceof HTMLElement) 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);
const inListing = (function() {
for (let elem = img; elem != null; elem = elem.parentNode) if (elem.tagName == 'DIV') {
if (elem.classList.contains('group_image')) return true;
if (elem.classList.contains('box_image')) return false;
}
throw 'Unexpected cover context';
})();
const isAlternateCover = !inListing && groupId > 0 && (id => (id = /^cover_(\d+)$/.exec(id)) != null && parseInt(id[1]) > 0)(img.id);
let sticker;
function editOnClick(elem, lookupFirst = false) {
if (!(elem instanceof HTMLElement)) return;
if (noEditPerms || !ajaxApiKey || readOnly || noAutoLookups) lookupFirst = false;
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;
};
if (lookupFirst) imageHostHelper.then(ihh => { setTooltip(img, 'Auto cover lookup on click') });
}
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 (isAlternateCover && groupId > 0) 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(evt) {
console.assert(evt instanceof MouseEvent);
for (let tgt = evt.relatedTarget; tgt instanceof HTMLElement; tgt = tgt.parentNode)
if (tgt == evt.currentTarget) 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);
}
imageUrl = new URL(imageDetails.src || imageUrl);
if (imageDetails.localProxy) setNewSrc(img, imageUrl);
const isPreferredHost = Array.isArray(preferredHosts) && preferredHosts.includes(imageUrl.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.type
|| preferredTypes.some(type => imageDetails.type.toLowerCase() == type);
const friendlyHost = getHostFriendlyName(imageUrl.href);
const resolution = span(imageDetails.width + '×' + imageDetails.height, 'resolution', isResolutionFine),
size = span(formattedSize(imageDetails.size), 'size', isSizeOK),
type = span(imageDetails.type, 'mime-type', isTypeOK);
const domain = imageUrl.hostname.split('.').slice(-2).join('.');
let host, lookup, downsize, rehost;
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)) sticker.style.opacity = 0 };
if (imageDetails.type) addStickerItems(1, type);
} else {
function keyHandlers(evt) {
if (evt.ctrlKey) evt.stopImmediatePropagation(); else return;
const listIndexes = click2goHostLists.map((_, listNdx) => getDomainListIndex(imageUrl.hostname, listNdx));
const dialog = document.createElement('DIALOG');
dialog.innerHTML = `
<form method="dialog">
<div><span><b>List entry:</b></span> <div class="domain" style="font-family: monospace; display: inline-block;" /></div><br><br>
<div>
<b>On lists:</b>
<label style="cursor: pointer;" title="Blacklisted domains are always excluded from batch processing. Doesn't apply to downsizing and lookup tasks."><input name="lists" value="blacklist" type="radio" /> Blacklist</label>
<label style="cursor: pointer;" title="Whitelisted domains are included in batch processing."><input name="lists" value="whitelist" type="radio" /> Whitelist</label>
<label style="cursor: pointer;" title="Images at bad domains are always considered invalid and an attempt to lookup new cover image is made."><input name="lists" value="badlist" type="radio" /> Badlist</label>
</div><br>
<div>
<input value="Update" type="button"><input value="Remove from all lists" type="button"><input value="Close" type="button">
</div>
</form>`;
dialog.style = 'padding: 1rem; position: fixed; top: 40%; left: 0; right: 0; margin-left: auto; margin-right: auto; z-index: 9999; box-shadow: 2pt 2pt 5pt gray;';
dialog.onclose = evt => { document.body.removeChild(evt.currentTarget) };
const radios = dialog.querySelectorAll('input[name="lists"][type="radio"]'),
buttons = dialog.querySelectorAll('input[type="button"]'),
domain = dialog.querySelector('div.domain');
function updateParts(index) {
domain.dataset.index = index;
for (let dp of domainParts) {
const active = parseInt(dp.dataset.index) >= index;
dp.style.opacity = active ? 1 : 0.5;
dp.style.fontWeight = active ? 'bold' : 'normal';
dp.style.color = active ? 'mediumblue' : null;
}
}
imageUrl.hostname.split('.').forEach(function(domainPart, index, arr) {
const span = document.createElement('SPAN'), notLast = index < arr.length - 1;
span.textContent = domainPart;
span.dataset.index = index;
if (notLast) {
span.style.cursor = 'pointer';
span.onclick = evt => { updateParts(parseInt(evt.currentTarget.dataset.index)) };
}
domain.append(span);
if (notLast) domain.append('.');
});
const domainParts = domain.getElementsByTagName('SPAN');
updateParts(Math.min(Math.max(domainParts.length - 2, 0), 1));
const inList = listIndexes.findIndex(index => index >= 0);
if (inList >= 0) radios[inList].checked = true; else {
buttons[0].disabled = true;
for (let radio of radios) radio.onchange = () => { buttons[0].disabled = false };
}
buttons[0].onclick = function(evt) {
radios.forEach(function(radio, index) {
if (radio.checked && listIndexes[index] < 0) {
click2goHostLists[index].push(Array.prototype.slice.call(domainParts, parseInt(domain.dataset.index))
.map(domainPart => domainPart.textContent).join('.'));
GM_setValue('click2go_' + radio.value, click2goHostLists[index].sort());
} else if (!radio.checked && listIndexes[index] >= 0) {
click2goHostLists[index].splice(listIndexes[index], 1);
GM_setValue('click2go_' + radio.value, click2goHostLists[index]);
}
});
dialog.close();
};
if (inList < 0) buttons[1].style.display = 'none';
buttons[1].onclick = function(evt) {
radios.forEach(function(radio, index) {
if (listIndexes[index] >= 0) {
click2goHostLists[index].splice(listIndexes[index], 1);
GM_setValue('click2go_' + radio.value, click2goHostLists[index]);
}
});
dialog.close();
};
buttons[2].onclick = evt => { dialog.close() };
document.body.append(dialog);
dialog.showModal();
return false;
}
function getHostTooltip() {
let tooltip = 'Hosted at ' + imageUrl.hostname;
if (imageDetails.localProxy) tooltip += ' (locally proxied)';
if (isOnDomainList(imageUrl.hostname, 2)) tooltip += ' (bad host)';
else if (isOnDomainList(imageUrl.hostname, 0)) tooltip += ' (blacklisted from batch rehosting)';
else if (isOnDomainList(imageUrl.hostname, 1)) tooltip += ' (whitelisted for batch rehosting)';
if (isOnDomainList(imageUrl.hostname, 2)) tooltip += '\n(look up different version on simple click)';
else if (!inListing || !isOnDomainList(imageUrl.hostname, 0))
tooltip += '\n(rehost to site preferred host on simple click)';
return tooltip + `
Ctrl + click to manage lists
(Ctrl +) middle click to open (full) image domain in new window`;
}
sticker.style.backgroundColor = '#ae2300';
sticker.style.opacity = 2/3;
sticker.onmouseleave = img.onmouseleave = evt => { if (isOutside(evt)) sticker.style.opacity = 2/3 };
if (inListing && !isAlternateCover && 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 = 0xFFFF20 + Math.round((0xC0 - 0x20) * color);
resolution.style.color = '#' + color.toString(16).padStart(6, '0');
}
if (/*!isResolutionAcceptable && */!isAlternateCover && groupId > 0 && !readOnly && !noAutoLookups)
lookup = resolution;
else setTooltip(resolution, (isResolutionAcceptable ? 'Mediocre' : 'Poor') + ' image quality (resolution)');
}
if (!isPreferredHost) {
host = span(friendlyHost || 'XTRN', 'unpreferred-host', false);
if (imageDetails.localProxy) host.classList.add('local-proxy');
if (isOnDomainList(imageUrl.hostname, 0)) {
host.style.color = '#ffd';
if (inListing) host.classList.add('blacklisted-from-click2go');
} else if (isOnDomainList(imageUrl.hostname, 1)) {
if (inListing) host.classList.add('whitelisted');
} else if (!isOnDomainList(imageUrl.hostname, 2)) host.style.color = '#ffa';
setTooltip(host, getHostTooltip());
host.onclick = keyHandlers;
host.onauxclick = function(evt) {
if (evt.button != 1) return;
GM_openInTab(evt.ctrlKey ? imageUrl.origin : imageUrl.protocol + '//' + domain, false);
evt.preventDefault();
return false;
};
addStickerItems(-1, host);
if (!readOnly) rehost = host;
}
if (!isTypeOK) {
type.onclick = function(evt) {
if (!evt.shiftKey || !confirm(`This will add "${imageDetails.type}" to whitelisted image types`))
return false;
preferredTypes.push(imageDetails.type);
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' && !readOnly)
downsize = size;
if (!isAlternateCover && groupId > 0 && !noEditPerms && ajaxApiKey) 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 = '250ms';
elem.onmouseenter = elem.onmouseleave = function(evt) {
if (evt.relatedTarget == evt.currentTarget) return false;
evt.currentTarget.style.textShadow = evt.type == 'mouseenter' ? '0 0 5px lime' : 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;
queryAjaxAPI('torrentgroup', { id: groupId }).then(function(torrentGroup) {
if (lookup == resolution && ![1].includes(torrentGroup.group.categoryId) && isResolutionAcceptable)
return !isPreferredHost ? ihh.rehostImageLinks([imageUrl.href], true, false, true).then(ihh.singleImageGetter)
.then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl, summary).then(function(response) {
console.log('[Cover Inspector]', response);
setNewSrc(img, imageUrl);
return setSticker(imageUrl);
})) : (img.style.opacity = 1);
return coverLookup(torrentGroup, ihh, lookup == resolution).then(imageUrls => (lookup == resolution ?
getImageDetails(imageUrls[0]).catch(reason => null) : null).then(function(newImageDetails) {
switch (lookup) {
case resolution:
console.assert(acceptableResolution > 0);
if (newImageDetails != null && !(newImageDetails.width * newImageDetails.height > imageDetails.width * imageDetails.height))
return Promise.reject(`New cover found in no better resolution (${newImageDetails.width}×${newImageDetails.height})`);
summary = 'Automated attempt to lookup better quality cover';
// if (newImageDetails != null)
// summary += ` (${imageDetails.width}×${imageDetails.height} → ${newImageDetails.width}×${newImageDetails.height})`;
break;
default:
summary = 'Automated attempt to lookup cover';
}
return ihh.rehostImageLinks(imageUrls).then(ihh.singleImageGetter)
.then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl, summary).then(function(response) {
console.log('[Cover Inspector]', response);
setNewSrc(img, imageUrl);
const status = setSticker(imageUrl);
if (inListing && autoOpenSucceed) openGroup(torrentGroup);
if (newImageDetails == null) return status.then(function(status) {
if ((status & 0b100) == 0 || (status & 0b10) == 0 && [1].includes(torrentGroup.group.categoryId))
return Promise.reject('New cover found in poor quality');
if (inCoversCollage('poor', torrentGroup)) removeFromCoversCollage('poor', torrentGroup);
});
const loDim = Math.min(newImageDetails.width, newImageDetails.height);
if (loDim < acceptableResolution || loDim < fineResolution && [1].includes(torrentGroup.group.categoryId))
return Promise.reject('New cover found in poor quality');
if (inCoversCollage('poor', torrentGroup)) removeFromCoversCollage('poor', torrentGroup);
}));
})).catch(function(reason) {
if (lookup == resolution && [1].includes(torrentGroup.group.categoryId)
&& !inCoversCollage('poor', torrentGroup)) addToCoversCollage('poor', torrentGroup.group.id);
return Promise.reject(reason);
});
}).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 ? (isResolutionAcceptable ? 'Mediocre' : '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(imageUrl.href, 2160, 90).then(output => output.size < imageDetails.size ?
ihh.rehostImages([output.uri]).then(ihh.singleImageGetter).then(function(imageUrl) {
summary = 'Automated cover downsize';
if (!isSizeOK) summary += ` (${formattedSize(imageDetails.size)} → ${formattedSize(output.size)})`;
return setGroupImage(groupId, imageUrl, summary).then(function(response) {
console.log('[Cover Inspector]', response);
setNewSrc(img, imageUrl);
setSticker(imageUrl);
});
}) : 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(rehost, function(evt) {
evt.stopImmediatePropagation();
if (evt.ctrlKey) return keyHandlers(evt);
if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true;
rehost = evt.currentTarget;
img.style.opacity = 0.3;
summary = 'Automated cover rehost';
//summary += ' (' + imageUrl.hostname + ')';
getImageMax(imageUrl.href).then(maxImgUrl => ihh.rehostImageLinks([maxImgUrl], true, false, true)
.then(ihh.singleImageGetter)).then(imageUrl => setGroupImage(groupId, imageUrl, summary).then(function(response) {
console.log('[Cover Inspector]', response);
setNewSrc(img, imageUrl);
setSticker(imageUrl);
})).catch(function(reason) {
ihh.logFail(`groupId ${groupId} cover rehost failed: ${reason}`);
img.style.opacity = 1;
rehost.disabled = false;
}).then(() => { counterDecrement('process-covers-countdown', tableIndex) });
});
});
}
sticker.title = imageUrl.href; //setTooltip(sticker, imageUrl.href);
sticker.onmouseenter = img.onmouseenter = evt => { sticker.style.opacity = 1 };
img.after(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;
if (groupId > 0) if (!isAlternateCover) editOnClick(sticker, true); else if (GM_getValue('auto_remove_invalid', true)) {
let div = img.parentNode;
if (div != null && (div = div.parentNode) != null) {
let rmCmd = div.querySelector('span.remove_cover_art > a');
if (rmCmd != null && (rmCmd.parentNode.previousSibling == null || !rmCmd.parentNode.previousSibling.textContent.trim())
&& typeof rmCmd.onclick == 'function' && (rmCmd = /\bajax\.get\((.+?)\)/.exec(rmCmd.onclick.toString())) != null) {
eval(rmCmd[0]);
div.remove();
return -1;
}
}
}
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'));
setTooltip(sticker, reason);
img.after(sticker);
img.dataset.statusFlags = (1 << 8).toString(2).padStart(9, '0');
return 1 << 8;
});
}
if (groupId > 0 && !isAlternateCover) imageHostHelper.then(function(ihh) {
img.classList.add('drop');
img.ondragover = evt => false;
if (img.clientWidth > 100) img.ondragenter = img[`ondrag${'ondragexit' in img ? 'exit' : 'leave'}`] = function(evt) {
if (evt.relatedTarget == evt.currentTarget) return false;
evt.currentTarget.parentNode.parentNode.style.backgroundColor = evt.type == 'dragenter' ? '#7fff0040' : 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).then(status => { updateCoverCollages(status, groupId) });
})).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 (!isAlternateCover && groupId > 0) editOnClick(img, true);
return Promise.resolve(0);
}
const recoverableHttpErrors = [/*0, */500, 502, 503, 504, 520, /*521, */522, 523, 524, 525, /*526, */527, 530];
const dcApiRateControl = { }, dcApiRequestsCache = new Map, bpRequestsCache = new Map;
const requestsCache = new Map, mbRequestsCache = new Map, caRequestsCache = new Map;
let mbLastRequest = null, bpAccessToken = null, spfAccessToken = null;
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());
function cseSearch(album, artist, country = GM_getValue('cse_country', 'us')) {
if (!album) return Promise.reject('Album missing');
const origin = 'https://covers.musichoarders.xyz', getAuth = () => (async function(au, cu) {
const e = Math.floor(Math.floor(new Date().getTime() / 1000) / cu);
const t = await crypto.subtle.importKey('raw', new TextEncoder().encode(au),
{ name: 'HMAC', hash: { name: 'SHA-256' } }, !0, ['sign']);
const n = await crypto.subtle.sign('HMAC', t, new Int32Array([e]).buffer);
return btoa(String.fromCharCode(...new Uint8Array(n)));
})('yUzpZk3QDoq6ztdbTB9Zx9MwTB4SSwtj', 150), apiHeaders = headers => Object.assign({
'Accept': 'application/json', 'Referer': origin + '/', 'Origin': origin,
'X-Session': crypto.randomUUID().replaceAll('-', ''), 'X-Page-Query': '', 'X-Page-Referrer': '',
'X-Requested-With': 'XMLHttpRequest',
}, headers);
return new Promise(function(resolve, reject) {
GM_xmlhttpRequest({
method: 'GET', url: origin + '/api/info', responseType: 'json', headers: apiHeaders(),
onload: function(response) {
if (response.status >= 200 && response.status < 400) try {
if (!response.response.sources || response.response.sources.length <= 0)
throw 'Assertion failed: Sources not found';
console.log('CSE API info:', response.response);
GM_setValue('cse_api_info', response.response);
resolve(response.response);
} catch(e) { reject(e) } else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).catch(function(reason) {
console.warn('CSE API Info failed:', reason);
return GM_getValue('cse_api_info', null);
}).then(function(apiInfo) {
const search = sources => getAuth().then(auth => new Promise((resolve, reject) => (function request(retry = 0) {
if (retry < 60) GM_xmlhttpRequest({
method: 'POST', url: origin + '/api/search', responseType: 'text', headers: apiHeaders({
'Accept': 'text/plain',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + auth,
}),
data: JSON.stringify({ album: album, artist: artist || '', country: country, sources: sources }),
onload: function(response) {
if (response.status >= 200 && response.status < 400) resolve(response.responseText.split(/(?:\r?\n)+/).map(function(line) {
try { if (line.trim() && (line = JSON.parse(line)).type == 'cover') {
for (let prop of ['type', 'cache']) delete line[prop];
return line;
} } catch(e) { console.warn(e, 'Line:', line) }
}).filter(Boolean));
else if (recoverableHttpErrors.includes(response.status)) setTimeout(request, 1000, retry + 1);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}); else reject('CSE request retry limit reached');
})()));
let sources = apiInfo != null && apiInfo.sources ?
apiInfo.sources.filter(source => source.enabled).map(source => source.id) : [ ];
if (sources.length <= 0) sources = [
'applemusic', 'itunes', 'qobuz', 'deezer', 'tidal', 'amazonmusic', 'bandcamp',
'gracenote', 'soundcloud', 'musicbrainz', 'spotify', 'vgmdb', 'bugs', 'flo', 'netease',
'linemusic', 'recochoku', 'kugou', 'gaana', 'discogs', 'soulseek', 'lastfm',
'metalarchives', 'fanarttv', 'melon', 'ototoy', 'kkbox', 'beatport', 'booth', 'thwiki',
];
let batchSize = apiInfo != null && apiInfo.activeSourceLimit || 12;
batchSize = Math.ceil(sources.length / Math.ceil(sources.length / batchSize));
if (!country || apiInfo != null && apiInfo.countries && apiInfo.countries.length > 0
&& !apiInfo.countries.some(c => c.toLowerCase() == country.toLowerCase())) country = 'us';
const searchWorkers = [ ];
for (let offset = 0; offset < sources.length; offset += batchSize)
searchWorkers.push(search(sources.slice(offset, offset + batchSize)));
return Promise.all(searchWorkers).then(results =>
(results = Array.prototype.concat.apply([ ], results)).length > 0 ? results : Promise.reject('No results'));
});
}
function coverLookup(torrentGroup, ihh, qualityAccent = false) {
if (!torrentGroup || !ihh) throw 'Invalid argument';
const namedFn = (fn, name) => Object.defineProperty(fn, 'name', { value: name, configurable: true });
const namesToSearchTerm = names => names.filter(Boolean).map(name => '"' + name + '"').join(' ');
let qA = GM_getValue('image_lookup_quality_accent');
if (qA && typeof (qA = { 'always': true, 'never': false }[qA.toLowerCase()]) == 'Boolean') qualityAccent = qA;
let lookupWorkers = [ ], workersOrder;
switch (torrentGroup.group.categoryId) {
case 1: { // Music category
const strippers = [
/^(?:Not On Label|Self[\s\-]Released|None)$|(?:\s+\b(?:Record(?:ing)?s)\b|,?\s+(?:Ltd|Inc|Co)\.?)+$|[\s\-]+/ig,
/^(?:None)$|[\s\-]+/ig,
].map(rx => idStr => idStr && idStr.trim().replace(rx, '').toLowerCase() || undefined);
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 mainArtist = (function() {
let mainArtist = 'dj' in torrentGroup.group.musicInfo && torrentGroup.group.musicInfo.dj[0];
if (!mainArtist && torrentGroup.group.releaseType != 7 && 'artists' in torrentGroup.group.musicInfo)
mainArtist = torrentGroup.group.musicInfo.artists[0];
if (mainArtist) return mainArtist.name;
})();
const barcodes = Array.isArray(torrentGroup.torrents) && torrentGroup.torrents.length > 0 ? (function() {
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;
});
return (barcodes = barcodes.filter(Boolean)).length > 0 ?
Promise.resolve(Array.prototype.concat.apply([ ], barcodes)) : Promise.reject('No torrents with barcode/UPC');
})() : Promise.reject('Cover lookup by barcode/UPC not available');
const allLabelsCatNos = (function() {
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;
})();
workersOrder = qualityAccent ? [
'itunesSearchByUPC', 'bpSearchByUPC', 'spfSearchByUPC', 'mbSearchByBarcode', 'dcSearchByBarcode',
'mbSearchByDiscId', 'bpSearchByArtistAlbumStrict',
'mbSearchByLabelCatno', 'dcSearchByLabelCatno',
'getImagesFromWikiBodyLinks',
'mbSearchByArtistAlbum', 'dcSearchByArtistMaster', 'dcSearchByArtistRelease',
'spfSearchByArtistAlbum', 'itunesSearchByArtistAlbum', 'bcSearchByArtistAlbum', 'bpSearchByArtistAlbum',
] : [
'itunesSearchByUPC', 'bpSearchByUPC', 'mbSearchByBarcode', 'spfSearchByUPC',
'itunesSearchByArtistAlbum',
'bpSearchByArtistAlbumStrict', 'mbSearchByDiscId',
'mbSearchByLabelCatno', 'dcSearchByBarcode', 'bcSearchByArtistAlbum',
'getImagesFromWikiBodyLinks',
'dcSearchByLabelCatno',
'mbSearchByArtistAlbum', 'spfSearchByArtistAlbum',
'dcSearchByArtistMaster', 'dcSearchByArtistRelease',
'bpSearchByArtistAlbum',
];
// ####################################### Extract from desc. links #######################################
lookupWorkers.push(function getImagesFromWikiBodyLinks() {
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 Discogs, req. credentials ###############################
const dcAuth = (function() {
const [token, consumerKey, consumerSecret] =
['discogs_api_token', 'discogs_api_consumerkey', 'discogs_api_consumersecret'].map(name => GM_getValue(name));
return token ? 'token=' + token : consumerKey && consumerSecret ?
`key=${consumerKey}, secret=${consumerSecret}` : undefined;
})();
if (dcAuth) {
function apiRequest(endPoint, params) {
if (endPoint) endPoint = new URL(endPoint, 'https://api.discogs.com'); else return Promise.reject('No endpoint provided');
if (params instanceof URLSearchParams) endPoint.search = params;
else if (typeof params == 'object') for (let key in params) endPoint.searchParams.set(key, params[key]);
else if (params) endPoint.search = new URLSearchParams(params);
const cacheKey = endPoint.pathname.slice(1) + endPoint.search;
if (dcApiRequestsCache.has(cacheKey)) return dcApiRequestsCache.get(cacheKey);
const reqHeaders = {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Authorization': 'Discogs ' + dcAuth,
};
const request = new Promise((resolve, reject) => (function request(retry = 0) {
if (retry >= 60) return reject('Retry limit exceeded');
const now = Date.now(), 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: endPoint, responseType: 'json', headers: reqHeaders,
onload: function(response) {
let 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) resolve(response.response);
else if (response.status == 429) {
console.warn(defaultErrorHandler(response), response.response.message, '(' + retry + ')', `Rate limit used: ${requestsUsed}/60`);
postpone();
} else if (recoverableHttpErrors.includes(response.status)) setTimeout(request, 1000, retry + 1);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}); else postpone();
})());
dcApiRequestsCache.set(cacheKey, request);
return request;
}
function search(type, queryParams, strictReleaseMatch = false) {
if (!type || !queryParams) throw 'Invalid argument';
const searchParams = new URLSearchParams(queryParams);
if (type) searchParams.set('type', type = type.toLowerCase());
searchParams.set('sort', 'score');
searchParams.set('sort_order', 'desc');
searchParams.set('per_page', 100);
return apiRequest('database/search', searchParams).then(function(response) {
function getFromResults(results) {
if (!results || results.length <= 0) return Promise.reject('No matches');
const coverImages = results.map(result => result.cover_image || singleResultGetter(result.images))
.filter(coverImage => coverImage && !coverImage.endsWith('/spacer.gif'));
return coverImages.length > 0 ? coverImages : Promise.reject('None of matched 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 apiRequest('masters/' + masterIds).then(masterRelease => masterRelease.images && masterRelease.images.length > 0 ?
masterRelease.images.map(image => image.uri || image.resource_url) : Promise.reject('No cover image for master release'));
//return ihh.imageUrlResolver('https://www.discogs.com/master/' + masterIds).then(singleResultGetter);
}
let results = response.results, masterIds;
if (results && results.length > 0) switch (type) {
case 'release': {
const getMasterIds = results => new Set(results.map(result => result.master_id));
if (strictReleaseMatch || getMasterIds(results).size > 1) results = results.map(result =>
apiRequest('releases/' + result.id).catch(reason => null).then(function(release) {
const releaseYear = release != null && release.year || new Date(result.year).getUTCFullYear();
const releaseTracks = release != null && release.tracklist ? release.tracklist.filter(track =>
['track', 'index'].includes(track.type_)/* && !/^\d+\.\d+/.test(track.position)*/).length : -1;
if (torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentIds = ['remasterRecordLabel', 'remasterCatalogueNumber']
.map((prop, index) => torrent[prop].split(/[\/\|]+/).map(strippers[index]).filter(Boolean));
if (release != null && release.labels && release.labels.some(function(label) {
label = ['name', 'catno'].map((prop, index) => strippers[index](label[prop]));
return label[1] && label.every((id, index) => !id || torrentIds[index].includes(id));
})) return true;
const torrentTracks = audioFileCount(torrent);
if (torrentTracks > 0 && releaseTracks > 0 && torrentTracks != releaseTracks) return false;
return torrent.remasterYear > 0 && releaseYear > 0 || torrentTracks > 0 && releaseTracks > 0;
})) return result;
}));
return Promise.all(results).then(results => results.filter(Boolean)).then(function(results) {
if (results.length > 1) {
//if (strictReleaseMatch) return Promise.reject('Ambiguous results');
console.info('[Cover Inspector] Ambiguous Discogs results for lookup query (type=%s, queryParams=%o)', type, queryParams);
}
if ((masterIds = getMasterIds(results)).size > 1) return Promise.reject('Ambiguous results');
return getFromMR(masterIds).catch(reason => getFromResults(results));
});
break;
}
case 'master':
return results.length > 1 ? Promise.reject('Ambiguous results') : getFromResults(results);
break;
default: return Promise.reject('Unsupported search type');
} else return Promise.reject('No matches');
});
}
const dcLookupWorkers = [
namedFn(() => barcodes.then(barcodes => 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'))), 'dcSearchByBarcode'),
function dcSearchByLabelCatno() {
if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0)
return Promise.reject('Cover lookup by label/cat.no. not available');
if (allLabelsCatNos == null) return Promise.reject('No torrents with label/cat.no.');
return Promise.all(allLabelsCatNos.map(queryParams => search('release', queryParams, true).catch(reason => null)))
.then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls)
: Promise.reject('No covers found by label/cat.no.'));
},
function dcSearchByArtistMaster() {
const queryParams = { };
if (mainArtist) queryParams.artist = mainArtist; else if (torrentGroup.group.releaseType != 7)
return Promise.reject('Cover lookup by artist/album/year not available');
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 dcSearchByArtistRelease() {
const queryParams = { };
if (mainArtist) queryParams.artist = mainArtist; else if (torrentGroup.group.releaseType != 7)
return Promise.reject('Cover lookup by artist/album not available');
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);
},
];
if (qualityAccent) {
const discogsSearch = () => (function searchMethod(index = 0) {
return index < dcLookupWorkers.length ? dcLookupWorkers[index]().then(function(results) {
namedFn(discogsSearch, dcLookupWorkers[index].name);
return results;
}, reason => searchMethod(index + 1)) : Promise.reject('No matches');
})();
lookupWorkers.push(discogsSearch);
} else Array.prototype.push.apply(lookupWorkers, dcLookupWorkers);
} { // #################################### Ext. lookup at MusicBrainz ####################################
function apiRequest(endPoint, params) {
if (!endPoint) throw 'Endpoint is missing';
const url = new URL('/ws/2/' + endPoint.replace(/^\/+/g, ''), 'https://musicbrainz.org');
if (params) for (let key in params) url.searchParams.set(key, params[key]);
const cacheKey = url.pathname.slice(6) + url.search;
if (mbRequestsCache.has(cacheKey)) return mbRequestsCache.get(cacheKey);
url.searchParams.set('fmt', 'json');
const request = new Promise(function(resolve, reject) { (function request(reqCounter = 1) {
if (reqCounter > 60) return reject('Request retry limit exceeded');
if (mbLastRequest == Infinity) return setTimeout(request, 100, reqCounter);
const now = Date.now();
if (now <= mbLastRequest + 1000) return setTimeout(request, mbLastRequest + 1000 - now, reqCounter);
mbLastRequest = Infinity;
GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'json',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
mbLastRequest = Date.now();
if (response.status >= 200 && response.status < 400) resolve(response.response);
else if ([429, 430].includes(response.status)) setTimeout(request, 1000, reqCounter + 1);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
})() });
mbRequestsCache.set(cacheKey, request);
return request;
}
function getFrontCovers(type, id) {
if (!type || !id) return Promise.reject('Invalid argument');
const key = type + '/' + id;
if (caRequestsCache.has(key)) return caRequestsCache.get(key);
const request = new Promise(function(resolve, reject) {
GM_xmlhttpRequest({ method: 'GET', url: `https://coverartarchive.org/${type}/${id}`, responseType: 'json',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
if (response.status >= 200 && response.status < 400)
if ((response = response.response).images && response.images.length > 0) {
let frontCovers = response.images.filter(image =>
image.front || image.types && image.types.includes('Front'));
//if (frontCovers.length <= 0) frontCovers = response.images;
frontCovers = frontCovers.map(image => image.image).filter(Boolean);
if (frontCovers.length > 0) resolve(frontCovers); else reject('This entity has no front cover');
} else reject('No artwork for this id');
else reject(response.status == 404 ? 'No images for this entity' : defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
});
caRequestsCache.set(key, request);
return request;
}
function search(type, queryParams, strictReleaseMatch = false) {
if (!type || !queryParams) throw 'Invalid argument';
queryParams = Object.keys(queryParams).map(field => `${field}:"${queryParams[field]}"`).join(' AND ');
return apiRequest((type = type.toLowerCase()) + '/', { query: queryParams }).then(function(response) {
function getFromRG(releaseGroupIds) {
if (!releaseGroupIds || releaseGroupIds.size <= 0) return Promise.reject('No matches');
if (releaseGroupIds.size > 1) return Promise.reject('Ambiguous results');
return (releaseGroupIds = releaseGroupIds.values().next().value) ?
getFrontCovers('release-group', releaseGroupIds) : Promise.reject('No release group');
}
function rgFilter(releaseGroup) {
if ((function rgFilter(releaseGroup) {
if (!releaseGroup) return false;
const isPrimaryType = primaryType => releaseGroup['primary-type'] == primaryType;
const hasSecondaryType = secondaryType => 'secondary-types' in releaseGroup
&& releaseGroup['secondary-types'].includes(secondaryType);
//if (['Audiobook', 'Spokenword', 'Audio drama'].some(hasSecondaryType) return false;
const releaseType = {
1: 'Album', 3: 'Soundtrack', 5: 'EP', 6: 'Anthology', 7: 'Compilation', 9: 'Single',
11: 'Live album', 13: 'Remix', 14: 'Bootleg', 15: 'Interview', 16: 'Mixtape', 17: 'Demo',
18: 'Concert Recording', 19: 'DJ Mix', 21: 'Unknown',
}[torrentGroup.group.releaseType];
console.assert(releaseType != undefined);
switch (releaseType) {
case 'Album': return ['Album', 'EP'].some(isPrimaryType)/* && !hasSecondaryType('Compilation')*/;
case 'Single': return ['Single', 'EP'].some(isPrimaryType);
case 'EP': return ['EP', 'Single'].some(isPrimaryType);
case 'Live album': case 'Concert Recording': return !isPrimaryType('Single') && hasSecondaryType('Live');
case 'Soundtrack': return /*!isPrimaryType('Single') && */hasSecondaryType('Soundtrack');
case 'Anthology': case 'Compilation': return !isPrimaryType('Single') && hasSecondaryType('Compilation');
case 'Remix': return /*!isPrimaryType('Single') && */hasSecondaryType('Remix');
case 'DJ Mix': return /*!isPrimaryType('Single') && */hasSecondaryType('DJ-mix');
case 'Demo': return /*!isPrimaryType('Single') && */hasSecondaryType('Demo');
case 'Mixtape': return /*!isPrimaryType('Single') && */hasSecondaryType('Mixtape/Street');
case 'Interview': return /*!isPrimaryType('Single') && */hasSecondaryType('Interview');
case 'Bootleg': //return !isPrimaryType('Single')/* && hasSecondaryType('Bootleg')*/;
}
return Boolean(releaseType);
})(releaseGroup)) return true;
console.log('[Cover Inspector] rgFilter(%o) returns false: releaseType=%d',
releaseGroup, torrentGroup.group.releaseType,
document.location.origin + '/torrents.php?id=' + torrentGroup.group.id,
'https://musicbrainz.org/release-group/' + releaseGroup.id);
return false;
}
if (response.count > 0) switch (type) {
case 'release': {
let releases = response.releases, releaseGroupIds;
if (!releases) return Promise.reject('No matches (renounced)');
const getReleaseGroupIds = releases =>
(releaseGroupIds = new Set(releases.map(release => release['release-group'].id)));
if ((strictReleaseMatch || getReleaseGroupIds(releases).size > 1)
&& getReleaseGroupIds(releases = releases.filter(function releaseFilter(release) {
if ((function releaseFilter(release) {
if (strictReleaseMatch && 'release-group' in release && !rgFilter(release['release-group'])) return false;
let releaseYear = new Date(release.date);
releaseYear = isNaN(releaseYear) ? undefined : releaseYear.getUTCFullYear();
return torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentIds = ['remasterRecordLabel', 'remasterCatalogueNumber']
.map((prop, index) => torrent[prop].split(/[\/\|]+/).map(strippers[index]).filter(Boolean));
if ('label-info' in release && release['label-info'].some(function(labelInfo) {
labelInfo = [labelInfo.label && labelInfo.label.name, labelInfo['catalog-number']]
.map((prop, index) => strippers[index](prop));
return labelInfo[1] && labelInfo.every((id, index) => !id || torrentIds[index].includes(id));
})) return true;
const torrentTracks = audioFileCount(torrent);
if (torrentTracks > 0 && release['track-count'] > 0 && torrentTracks != release['track-count']) return false;
return torrent.remasterYear > 0 && releaseYear > 0 || torrentTracks > 0 && release['track-count'] > 0;
});
})(release)) return true;
console.log('[Cover Inspector] releaseFilter(%o) returns false: torrents=%o', release, torrentGroup.torrents,
document.location.origin + '/torrents.php?id=' + torrentGroup.group.id, 'https://musicbrainz.org/release/' + release.id);
return false;
})).size > 1) return Promise.reject('Ambiguous results');
return getFromRG(releaseGroupIds).catch(reason => releases.length > 0 ? Promise.all(releases.map(function(release) {
const coverArtArchive = release['cover-art-archive'];
if (coverArtArchive && coverArtArchive.count <= 0) return Promise.resolve(null);
return getFrontCovers('release', release.id).then(singleResultGetter, reason => null);
})).then(frontCovers => (frontCovers = frontCovers.filter(Boolean)).length > 0 ?
frontCovers : Promise.reject('None of results has front cover')) : Promise.reject('No matches'));
}
case 'release-group': {
let releaseGroups = response['release-groups'];
if (!releaseGroups) return Promise.reject('No matches (renounced)');
if (strictReleaseMatch) releaseGroups = releaseGroups.filter(rgFilter);
return getFromRG(new Set(releaseGroups.map(releaseGroup => releaseGroup.id)));
}
default: return Promise.reject('Unsupported search type');
} else return Promise.reject('No matches');
});
}
const mbLookupWorkers = [
namedFn(() => barcodes.then(barcodes => 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'))), 'mbSearchByBarcode'),
function mbSearchByDiscId() {
if (typeof unsafeWindow != 'object' || !['lookupByToc', 'tocEntriesToMbTOC', 'mbComputeDiscID']
.every(prop => typeof(unsafeWindow[prop]) == 'function'))
return Promise.reject('CD TOC lookup endpoints not available');
const torrents = torrentGroup.torrents.filter(torrent => torrent.media == 'CD'
&& torrent.format == 'FLAC' && torrent.encoding == 'Lossless' && torrent.hasLog);
if (torrents.length <= 0) return Promise.reject('Cover lookup by CD TOC not available');
const safeRgId = release => release && 'release-group' in release ? release['release-group'].id : undefined;
const isConsistent = (results, ndx, arr) => safeRgId(results[0]) == safeRgId(arr[0][0]);
const mediaCD = media => !media.format || /\b(?:H[DQ])?CD\b/.test(media.format);
return Promise.all(torrents.map(torrent => unsafeWindow.lookupByToc(torrent.id, function(tocEntries, volumeNdx, totalDiscs) {
const mbTOC = unsafeWindow.tocEntriesToMbTOC(tocEntries);
if (mbTOC.length != mbTOC[1] - mbTOC[0] + 4) return Promise.reject('Missing or invalid TOC');
return apiRequest('discid/' + unsafeWindow.mbComputeDiscID(mbTOC), {
'media-format': 'all',
'inc': ['release-groups'].join('+'),
}).then(result => result.id && Array.isArray(result.releases) && (result = result.releases.filter(release =>
!release.media || release.media.filter(mediaCD).length == totalDiscs)).length > 0 ?
result.every((release, ndx, arr) => safeRgId(release) == safeRgId(arr[0])) ?
result : Promise.reject('Inconsistent results') : Promise.reject('No matches'));
}).then(results => (results = results.filter(Boolean)).length > 0 && results.every(isConsistent) ?
Array.prototype.concat.apply([ ], results) : null))).then(function(results) {
if ((results = results.filter(Boolean)).length <= 0) return Promise.reject('No matches');
if (!results.every(isConsistent)) return Promise.reject('Inconsistent results');
results = Array.prototype.concat.apply([ ], results);
return getFrontCovers('release-group', safeRgId(results[0])).catch(reason => (function doIndex(index = 0) {
if (index < results.length) {
const coverArtArchive = results[index]['cover-art-archive'];
if (coverArtArchive && coverArtArchive.count <= 0) return doIndex(index + 1);
return getFrontCovers('release', results[index].id).catch(reason => doIndex(index + 1));
} else return Promise.reject('No covers found by CD TOC');
})());
});
},
function mbSearchByLabelCatno() {
if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0)
return Promise.reject('Cover lookup by label/cat.no. not available');
if (allLabelsCatNos == null) return Promise.reject('No torrents with label/cat.no.');
return Promise.all(allLabelsCatNos.map(queryParams => search('release', queryParams, true).catch(reason => null)))
.then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls)
: Promise.reject('No covers found by label/cat.no.'));
},
function mbSearchByArtistAlbum() {
const queryParams = { };
if (mainArtist) queryParams.artistname = mainArtist; else if (torrentGroup.group.releaseType != 7)
return Promise.reject('Cover lookup by artist/album/year not available');
queryParams.releasegroup = bareReleaseTitle(torrentGroup.group.name);
queryParams.firstreleasedate = torrentGroup.group.year;
return search('release-group', queryParams, true);
},
];
if (qualityAccent) {
const mbSearch = () => (function searchMethod(index = 0) {
return index < mbLookupWorkers.length ? mbLookupWorkers[index]().then(function(results) {
namedFn(mbSearch, mbLookupWorkers[index].name);
return results;
}, reason => searchMethod(index + 1)) : Promise.reject('No matches');
})();
lookupWorkers.push(mbSearch);
} else Array.prototype.push.apply(lookupWorkers, mbLookupWorkers);
} { // ####################################### Ext. lookup at iTunes #######################################
const apiRequest = (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');
(function request(retry = 0) {
if (retry < 100) 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 == '/search' && (results = results.filter(function(result) {
let releaseYear = new Date(result.releaseDate);
releaseYear = isNaN(releaseYear) ? undefined : releaseYear.getUTCFullYear();
return torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentTracks = audioFileCount(torrent);
if (torrentTracks > 0 && result.trackCount > 0 && torrentTracks != result.trackCount) return false;
return torrent.remasterYear > 0 && releaseYear > 0 && torrentTracks > 0 && result.trackCount > 0;
});
})).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+)/, '/4000x4000');
});
if ((artworkUrls = artworkUrls.filter(Boolean)).length > 0) resolve(artworkUrls); else reject('No matches');
} else reject('No matches'); else if (response.status == 403) setTimeout(request, 1000, retry + 1);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}); else reject('Retry limit exceeded');
})();
}) : Promise.reject('Invalid argument');
lookupWorkers.push(namedFn(() => barcodes.then(upcs => Promise.all(upcs.map(upc =>
apiRequest('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'))), 'itunesSearchByUPC'));
lookupWorkers.push(function itunesSearchByArtistAlbum() {
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 not available');
return apiRequest('search', {
term: namesToSearchTerm(artistNames.concat(albumTitle)),
attribute: 'mixTerm',
limit: 15,
}, artistNames.join(' & ').toLowerCase() == albumTitle.toLowerCase()
|| artistNames.join('').length + albumTitle.length < 15);
});
} { // ###################################### Ext. lookup at Spotify ######################################
const spfAuth = (function() {
const [clientId, clientSecret] = ['spotify_client_id', 'spotify_client_secret'].map(name => GM_getValue(name));
return clientId && clientSecret ? btoa(clientId + ':' + clientSecret) : undefined;
})();
const requestEndpoint = (server, endpoint, auth, params) => server && endpoint && auth ? new Promise(function(resolve, reject) {
const url = new URL(endpoint, 'https://' + server + '.spotify.com'), isPost = server.toLowerCase() != '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,
});
}) : Promise.reject('Invalid argument');
const search = queryParams => queryParams && typeof queryParams == 'object' ? (function getAuthToken() {
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)) throw 'Throwing expired or otherwise invalid Spotify cached token';
// console.info('[Cover Inspector] Re-using cached Spotify access token:', accessToken,
// 'expires at', new Date(accessToken.expires_at).toTimeString(),
// '(+' + ((accessToken.expires_at - Date.now()) / 1000 / 60).toFixed(2) + 'm)');
return Promise.resolve(accessToken);
} catch(e) {
console.info('[Cover Inspector]', e);
localStorage.removeItem('spotifyAccessToken');
}
if (spfAccessToken instanceof Promise) return spfAccessToken.then(accessToken => isTokenValid(accessToken) ?
Promise.resolve(accessToken) : Promise.reject('auth session failed')).catch(function(reason) {
spfAccessToken = null;
console.info('[Cover Inspector] Discarding Spotify access token:', reason);
return getAuthToken();
});
const timeStamp = Date.now();
return (spfAccessToken = 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)) {
try { localStorage.setItem('spotifyAccessToken', JSON.stringify(accessToken)) } catch(e) { console.warn(e) }
spfAccessToken = null;
console.log('[Cover Inspector] Spotify access token successfully set:',
accessToken, `(+${(Date.now() - accessToken.timestamp) / 1000}s)`);
} else {
spfAccessToken = null;
console.warn('Received invalid Spotify token:', accessToken);
return Promise.reject('invalid token received');
}
return accessToken;
}) : Promise.reject('Basic authorization not fully configured'));
})().then(authToken => requestEndpoint('api', 'v1/search', authToken.token_type + ' ' + authToken.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);
releaseYear = isNaN(releaseYear) ? undefined : releaseYear.getUTCFullYear();
return torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentFiles = audioFileCount(torrent);
if (torrentFiles > 0 && result.total_tracks > 0 && torrentFiles != result.total_tracks) return false;
return torrentFiles > 0 && result.total_tracks > 0 || torrent.remasterYear > 0 && releaseYear > 0;
});
});
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);
}
return (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)).length > 0 ? results : Promise.reject('No covers');
})) : Promise.reject('No query provided');
lookupWorkers.push(namedFn(() => barcodes.then(upcs => Promise.all(upcs.map(upc => search({ upc: upc }) // 7
.catch(reason => null))) .then(artworkUrls => (artworkUrls = artworkUrls.filter(Boolean)).length > 0 ?
Array.prototype.concat.apply([ ], artworkUrls) : Promise.reject('No covers found by UPC'))), 'spfSearchByUPC'));
lookupWorkers.push(function spfSearchByArtistAlbum() {
const queryParams = { };
if (mainArtist) queryParams.artist = mainArtist; else if (torrentGroup.group.releaseType != 7)
return Promise.reject('Cover lookup by artist/album not available');
queryParams.album = bareReleaseTitle(torrentGroup.group.name);
return search(queryParams);
});
} { // ###################################### Ext. lookup at Bandcamp ######################################
const search = (searchTerm, itemType = 'a') => searchTerm ? new Promise(function(resolve, reject) {
const url = new URL('https://bandcamp.com/search');
url.searchParams.set('q', searchTerm);
url.searchParams.set('item_type', itemType = itemType.toLowerCase());
GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'document', headers: { 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
let results = response.response.body.querySelectorAll('div.results > ul.result-items > li.searchresult');
if (results.length <= 0 || (results = Array.prototype.filter.call(results, function(result) {
let [title, artist, releaseYear, releaseTracks] = ['heading', 'subhead', 'released', 'length']
.map(className => result.querySelector('div.' + className));
if (title != null) title = title.textContent.trim(); else return false;
//if (bareReleaseTitle(title).toLowerCase() != torrentGroup.group.name.toLowerCase()) return false;
if (artist != null) artist = /^by (.+)$/i.exec(artist.textContent.trim());
if (artist != null) artist = artist[1]; else return false;
if (releaseYear != null) releaseYear = /\b(\d{4})\b/.exec(releaseYear.textContent);
releaseYear = releaseYear != null ? parseInt(releaseYear[1]) : undefined;
if (itemType == 't') releaseTracks = 1; else if (itemType == 'a') {
if (releaseTracks != null) releaseTracks = /\b(\d+)\s+tracks?\b/i.exec(releaseTracks.textContent);
releaseTracks = releaseTracks != null ? parseInt(releaseTracks[1]) : undefined;
}
return torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentTracks = audioFileCount(torrent);
if (torrentTracks > 0 && releaseTracks > 0 && torrentTracks != releaseTracks) return false;
return torrent.remasterYear > 0 && releaseYear > 0 && torrentTracks > 0 && releaseTracks > 0;
});
})).length <= 0) return reject('No matches'); else if (results.size > 1) {
console.info('[Cover Inspector] Ambiguous Bandcamp results for lookup query', searchTerm);
//return reject('Ambiguous results');
}
//console.debug('[Cover Inspector] Bandcamp search results for %o:', searchTerm, results);
Promise.all(results.map(function(result) {
const image = result.querySelector('a.artcont img');
return image != null && getImageMax(image.src).catch(reason => image);
})).then(results = results.filter(Boolean))
.then(results => { if (results.length > 0) resolve(results); 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 bcSearchByArtistAlbum() {
let searchTerm = [mainArtist, bareReleaseTitle(torrentGroup.group.name)];
if (!searchTerm[0]) return Promise.reject('Cover lookup by artist/album not available');
return search((searchTerm = namesToSearchTerm(searchTerm)), 'a').catch(reason =>
reason == 'No matches' && torrentGroup.torrents.map(audioFileCount).some(afc => afc == 1) ?
search(searchTerm, 't') : Promise.reject(reason));
});
} { // ###################################### Ext. lookup at Beatport ######################################
const queryAPI = (endPoint, params) => endPoint ? (function setAccessToken() {
const isTokenValid = accessToken => accessToken && accessToken.token_type
&& accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
return bpAccessToken instanceof Promise ? bpAccessToken.then(accessToken =>
isTokenValid(accessToken) ? accessToken : Promise.reject('expired or otherwise invalid')).catch(function(reason) {
bpAccessToken = null;
console.info('Discarding Beatsource access token:', reason);
return setAccessToken();
}) : (bpAccessToken = new Promise(function(resolve, reject) {
function haveToken({token}) {
if (!(token = {
token_type: token.tokenType,
access_token: token.accessToken,
timestamp: timeStamp,
expires_in: token.expiresIn,
expires_at: token.accessTokenExpires,
}).expires_at) token.expires_at = token.timestamp + (token.expires_in_ms || token.expires_in * 1000);
if (!isTokenValid(token)) {
console.warn('Received invalid Beatport token:', token);
return reject('invalid token received');
}
try { localStorage.setItem('beatportAccessToken', JSON.stringify(token)) } catch(e) { console.warn(e) }
console.log('Beatport access token successfully set:',
token, `(+${(Date.now() - token.timestamp) / 1000}s)`);
resolve(token);
}
if ('beatportAccessToken' in localStorage) try {
const accessToken = JSON.parse(localStorage.getItem('beatportAccessToken'));
if (!isTokenValid(accessToken)) throw 'Expired or otherwise invalid';
console.info('Re-using cached Beatport access token:', accessToken,
'expires at', new Date(accessToken.expires_at).toTimeString(),
'(+' + ((accessToken.expires_at - Date.now()) / 1000 / 60).toFixed(2) + 'm)');
return resolve(accessToken);
} catch(e) { localStorage.removeItem('beatportAccessToken') }
const timeStamp = Date.now(), urlBase = 'https://www.beatport.com/api/auth';
GM_xmlhttpRequest({ method: 'GET', url: urlBase + '/session', responseType: 'json',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
const getCookie = (responseHeaders, cookie) =>
(cookie = new RegExp(`^(?:set-cookie):\\s*${cookie}=(.+)$`, 'im')
.exec(responseHeaders)) && cookie[1].split(';').map(val => val.trim());
let cookie = getCookie(response.responseHeaders, '__Secure-next-auth\\.session-token');
if (cookie != null) return haveToken(response.response);
const postData = { };
if ((cookie = getCookie(response.responseHeaders, '__Host-next-auth\\.csrf-token')) != null)
postData.csrfToken = cookie[0].split('|')[0];
else return reject('Cookie not received');
if ((cookie = getCookie(response.responseHeaders, '__Secure-next-auth\\.callback-url')) != null)
postData.callbackUrl = cookie[0];
else return reject('Cookie not received');
GM_xmlhttpRequest({ method: 'POST', url: urlBase + '/callback/anonymous',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
data: new URLSearchParams(Object.assign(postData, { json: true })),
onload: function(response) {
if ((cookie = getCookie(responseHeaders, '__Secure-next-auth\\.session-token')) == null)
return reject('Cookie not received');
GM_xmlhttpRequest({ method: 'GET', url: urlBase + '/session', responseType: 'json',
cookie: '__Secure-next-auth.session-token=' + cookie[1],
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: ({response}) => { haveToken(response) },
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).catch(function(reason) {
if ('beatsourceAccessToken' in localStorage) try {
const accessToken = JSON.parse(localStorage.getItem('beatsourceAccessToken'));
if (!isTokenValid(accessToken)) throw 'Throwing expired or otherwise invalid Beatsource cached token';
// console.info('[Cover Inspector] Re-using cached Beatsource access token:', accessToken,
// 'expires at', new Date(accessToken.expires_at).toTimeString(),
// '(+' + ((accessToken.expires_at - Date.now()) / 1000 / 60).toFixed(2) + 'm)');
return Promise.resolve(accessToken);
} catch(e) { localStorage.removeItem('beatsourceAccessToken') }
if (bpAccessToken instanceof Promise) return bpAccessToken.then(accessToken => isTokenValid(accessToken) ?
Promise.resolve(accessToken) : Promise.reject('auth session failed')).catch(function(reason) {
bpAccessToken = null;
console.info('[Cover Inspector] Discarding Beatsource access token:', reason);
return getAuthToken();
});
const timeStamp = Date.now();
return new Promise(function(resolve, reject) {
GM_xmlhttpRequest({ method: 'GET', url: 'https://www.beatsource.com/', responseType: 'document',
headers: { 'Accept': 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
if (response.status < 200 || response.status >= 400) reject(defaultErrorHandler(response)); else {
let accessToken = response.response.getElementById('__NEXT_DATA__');
if (accessToken != null) try {
accessToken = JSON.parse(accessToken.text);
return resolve(Object.assign(accessToken.props.rootStore.authStore.user, {
apiHost: accessToken.runtimeConfig.API_HOST,
clientId: accessToken.runtimeConfig.API_CLIENT_ID,
recurlyPublicKey: accessToken.runtimeConfig.RECURLY_PUBLIC_KEY,
}));
} catch(e) { console.warn(e) }
if ((accessToken = /\b(?:btsrcSession)=([^\s\;]+)/m.exec(response.responseHeaders)) != null) try {
accessToken = JSON.parse(decodeURIComponent(accessToken[1]));
let sessionId = /\b(?:sessionId)=([^\s\;]+)/m.exec(response.responseHeaders);
if (sessionId != null) try { accessToken.sessionId = decodeURIComponent(sessionId[1]) }
catch(e) { console.warn(e) }
return resolve(accessToken);
} catch(e) { console.warn(e) }
reject('Beatsource OAuth2 access token could not be extracted');
}
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
}).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)) try {
localStorage.setItem('beatsourceAccessToken', JSON.stringify(accessToken));
bpAccessToken = null;
console.log('[Cover Inspector] Beatsource access token successfully set:',
accessToken, `(+${(Date.now() - accessToken.timestamp) / 1000}s)`);
} catch(e) { console.warn(e) } else {
bpAccessToken = null;
console.warn('[Cover Inspector] Received invalid Beatsource token:', accessToken);
return Promise.reject('invalid token received');
}
return accessToken;
});
}));
})().then(function(authToken) {
const catRoot = '/v4/catalog/';
const url = new URL(`${catRoot}${endPoint.replace(/^\/+|\/+$/g, '')}/`, 'https://api.beatport.com');
if (params) url.search = new URLSearchParams(params);
const cacheKey = url.pathname.slice(url.pathname.indexOf(catRoot) + catRoot.length) + url.search;
if (bpRequestsCache.has(cacheKey)) return bpRequestsCache.get(cacheKey);
const request = new Promise(function(resolve, reject) {
GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'json',
headers: {
'Accept': 'application/json',
'Authorization': authToken.token_type + ' ' + authToken.access_token,
'X-Requested-With': 'XMLHttpRequest',
},
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)) },
});
});
bpRequestsCache.set(cacheKey, request);
return request;
}) : Promise.reject('Endpoint is missing');
const search = (searchTerm, strict = true) => searchTerm ? queryAPI('search', {
q: searchTerm,
type: 'releases',
per_page: strict ? 50 : 1,
}).then(function(results) {
if (results.count <= 0 || (results = results.releases.filter(function(release) {
if ('release_type' in release && (parseInt(release.release_type) == 1 ?
![9, 5].includes(torrentGroup.group.releaseType) : torrentGroup.group.releaseType == 9)) return false;
let releaseYear = new Date(release.release_date || release.new_release_date || release.publish_date);
if (isNaN(releaseYear)) return false; else releaseYear = releaseYear.getUTCFullYear();
return torrentGroup.torrents.some(function(torrent) {
if (torrent.remasterYear > 0 && torrent.remasterYear != releaseYear) return false;
const torrentFiles = audioFileCount(torrent);
if (torrentFiles > 0 && release.track_count > 0 && torrentFiles != release.track_count) return false;
if (!strict) return torrent.remasterYear > 0 && releaseYear > 0
&& torrentFiles > 0 && release.track_count > 0;
if (!(torrent.remasterYear > 0 && releaseYear > 0 || torrentFiles > 0 && release.track_count > 0))
return false;
const torrentIds = ['remasterRecordLabel', 'remasterCatalogueNumber']
.map((prop, index) => torrent[prop].split(/[\/\|]+/).map(strippers[index]).filter(Boolean));
const releaseIds = [release.label && release.label.name, release.catalog_number]
.map((prop, index) => strippers[index](prop));
return releaseIds[1] && releaseIds.every((id, index) => !id || torrentIds[index].includes(id));
});
})).length <= 0) return Promise.reject('No matches'); else if (results.length > 1) {
//return reject('Ambiguous results');
console.info('[Cover Inspector] Ambiguous Beatport results for lookup query (searchTerm=%s)', searchTerm);
}
//console.debug('[Cover Inspector] Beatport search results for %s:', searchTerm, results);
return (results = results.filter(release => release.image && release.image.uri && ![
'0dc61986-bccf-49d4-8fad-6b147ea8f327', 'ab2d1d04-233d-4b08-8234-9782b34dcab8',
].some(imgId => release.image.uri.toLowerCase().endsWith(`/${imgId.toLowerCase()}.jpg`)))).length > 0 ?
results.map(release => release.image.uri.replace(/\/image_size\/\d+x\d+\//i, '/image/'))
: Promise.reject('No covers in matching releases found');
}) : Promise.reject('Search term is missing');
if ([
'acid', 'acid.house', 'bass', 'beats', 'breakbeat', 'breakcore', 'breaks', 'chillout', 'chillwave',
'chiptune', 'dance', 'dark.psytrance', 'deep.house', 'deep.tech', 'downtempo', 'drum.and.bass', 'dub',
'dub.techno', 'dubstep', 'ebm', 'electro', 'electro.house', 'electronic', 'garage.house', 'glitch',
'goa.trance', 'grime', 'hard.techno', 'hard.trance', 'hardcore.dance', 'house', 'idm', 'jungle',
'leftfield', 'minimal.house', 'minimal.techno', 'nu.disco', 'progressive.house', 'progressive.trance',
'psybient', 'psytrance', 'synth', 'tech.house', 'techno', 'trance', 'trap', 'tribal', 'trip.hop',
'uk.garage', 'uplifting.trance', 'vaporwave',
].some(tag => torrentGroup.group.tags.includes(tag))) {
lookupWorkers.push(namedFn(() => barcodes.then(upcs => Promise.all(upcs.map(upc => search(upc, false)
.catch(reason => null))).then(artworkUrls => (artworkUrls = artworkUrls.filter(Boolean)).length > 0 ?
Array.prototype.concat.apply([ ], artworkUrls) : Promise.reject('No covers found by UPC'))), 'bpSearchByUPC'));
const keywords = [mainArtist, bareReleaseTitle(torrentGroup.group.name)];
lookupWorkers.push(namedFn(() => search(namesToSearchTerm(keywords), true), 'bpSearchByArtistAlbumStrict'));
if (keywords[0] && torrentGroup.group.releaseType != 7)
lookupWorkers.push(namedFn(() => search(namesToSearchTerm(keywords), false), 'bpSearchByArtistAlbum'));
}
} { // ###################################### Ext. lookup at CSE ######################################
// if (mainArtist && torrentGroup.group.releaseType != 7) lookupWorkers.push(namedFn(() =>
// Promise.all(['us', 'jp'].map(country => cseSearch(bareReleaseTitle(torrentGroup.group.name), mainArtist, country))
// .map(promise => promise.catch(reason => null))).then(function(results) {
// if ((results = Array.prototype.concat.apply([ ], results.filter(Boolean)).filter((result, ndx, arr) =>
// result.bigCoverUrl && arr.findIndex(result2 => result2.bigCoverUrl == result.bigCoverUrl) == ndx)).length <= 0)
// return Promise.reject('No matches');
// // TODO: verify & return best match
// }), 'cseSearchByArtistAlbum'));
}
break;
}
case 3: { // Ebooks
qualityAccent = false;
workersOrder = ['grSearchById', 'grSearchByTitle1', 'grSearchByTitle2'];
{ // ###################################### Ext. lookup at Goodreads ######################################
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, '.');
const dummyCover = coverUrl => coverUrl && ['/nophoto/', '/books/1570622405l/50809027', '/images/no-cover.png']
.some(pattern => coverUrl.includes(pattern));
let results = ['div.BookCover__image img', 'div.editionCover > img', 'img#coverImage']
.reduce((elem, selector) => elem || response.response.body.querySelector(selector), null);
if (results != null && httpParser.test(results = results.src)) {
if (!dummyCover(results)) return resolve([grImageMax(results)]);
} else if ((results = response.response.querySelectorAll('table.tableList > tbody > tr')).length > 0) {
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) && !dummyCover(coverUrl))
return grImageMax(coverUrl);
}).filter(Boolean)).length > 0) return resolve(results);
} else return reject('No matches');
reject('No valid cover image for matched ebooks');
},
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(namedFn(() => search({ q: id }), 'grSearchById'));
}
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);
const rx = [
/(?:\s+(?:\((?:19|2\d)\d{2}\)|\[(?:19|2\d)\d{2}\]|\((?:epub|mobi|pdf)\)|\[(?:epub|mobi|pdf)\]))+$/ig,
/(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]))+$/ig,
], titles = [torrentGroup.group.name.replace(rx[0], '')];
lookupWorkers.push(namedFn(() => search({ q: titles[0] }), 'grSearchByTitle1'));
titles.push(titles[0].replace(rx[1], ''));
if (titles[1].length < titles[0].length)
lookupWorkers.push(namedFn(() => search({ q: titles[1] }), 'grSearchByTitle2'));
}
break;
}
}
if ((lookupWorkers = lookupWorkers.filter(worker => typeof worker == 'function')).length <= 0)
return Promise.reject('No available lookup methods for this release group');
if (qualityAccent) {
const results = lookupWorkers.map(lookupWorker => lookupWorker().then(results =>
(results = results.filter(Boolean)).length > 0 ? results : Promise.reject('No valid images found')));
return Promise.all(results.map(result => result.then(imageUrls =>
testImageQuality(imageUrls[0])).catch(reason => -1))).then(function(qRanks) {
const maxRank = Math.max(...qRanks);
// console.debug('[Cover Inspector] Quality ranking for group id %d lookup workers:', torrentGroup.group.id,
// maxRank, Object.assign.apply({ }, qRanks.map((qRank, index) => ({ [lookupWorkers[index].name]: qRank }))));
if (maxRank < 0) return Promise.reject('None of release identifiers was sufficient to find the cover');
let index = Math.min(...qRanks.map((qR, ndx) => qR < maxRank
|| (ndx = workersOrder.indexOf(lookupWorkers[ndx].name)) < 0 ? Infinity : ndx));
index = lookupWorkers.findIndex(lookupWorker => lookupWorker.name == workersOrder[index]);
console.assert(index >= 0, index);
if (index < 0 && (index = qRanks.indexOf(maxRank)) < 0)
return Promise.reject('Assertion failed: lookup worker index doesnot exist');
console.log('[Cover Inspector] Group id', torrentGroup.group.id, torrentGroup,
'covers lookup successfull, method name:', lookupWorkers[index].name, '[' + index + ']');
return results[index];
});
} else {
lookupWorkers.sort((a, b) => workersOrder.indexOf(a.name) - workersOrder.indexOf(b.name));
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 images found');
console.log('[Cover Inspector] Group id', torrentGroup.group.id, torrentGroup,
'covers lookup successfull, method name:', lookupWorkers[index].name, '[' + index + '/' +
lookupWorkers.length + '], results:', results);
return results;
}).catch(reason => lookupMethod(index + 1));
return Promise.reject('None of release identifiers was sufficient to find the cover');
})();
}
}
function updateCoverCollages(status, torrentGroup) {
(typeof torrentGroup == 'object' ? Promise.resolve(torrentGroup)
: queryAjaxAPI('torrentgroup', { id: torrentGroup || id })).then(function(torrentGroup) {
if ((status >> 7 & 0b11) == 0b11) for (let collageIndex of ['missing', 'invalid', 'investigate'])
if (inCoversCollage(collageIndex, torrentGroup)) removeFromCoversCollage(collageIndex, torrentGroup);
if ((status & 0b100) == 0 || (status & 0b10) == 0 && [1].includes(torrentGroup.group.categoryId)) {
if (!inCoversCollage('poor', torrentGroup)) addToCoversCollage('poor', torrentGroup.group.id);
} else if (inCoversCollage('poor', torrentGroup)) removeFromCoversCollage('poor', torrentGroup);
});
}
function updateImage(imageUrl, torrentGroup, img) {
if (!imageUrl || !torrentGroup) throw 'Invalid argument';
for (let collageIndex of ['missing', 'invalid', 'investigate'])
if (inCoversCollage(collageIndex, torrentGroup)) removeFromCoversCollage(collageIndex, torrentGroup);
if (img instanceof HTMLImageElement) {
setNewSrc(img, imageUrl);
inspectImage(img, torrentGroup.group.id).then(function(status) {
if ((status & 0b100) != 0 && ((status & 0b10) != 0 || ![1].includes(torrentGroup.group.categoryId))) {
if (inCoversCollage('poor', torrentGroup)) removeFromCoversCollage('poor', torrentGroup);
} else if ([1].includes(torrentGroup.group.categoryId))
if (!inCoversCollage('poor', torrentGroup)) addToCoversCollage('poor', torrentGroup.group.id);
}, reason => { console.warn('[Cover Inspector] inspectImage(', img, ') failed with reason', reason) });
} else testImageQuality(imageUrl).then(function(q) {
if (q >= ([1].includes(torrentGroup.group.categoryId) ? 2 : 1)) {
if (inCoversCollage('poor', torrentGroup)) removeFromCoversCollage('poor', torrentGroup);
} else if ([1].includes(torrentGroup.group.categoryId))
if (!inCoversCollage('poor', torrentGroup)) addToCoversCollage('poor', torrentGroup.group.id);
});
}
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]]).then(ihh.singleImageGetter).then(imageUrl =>
setGroupImage(torrentGroup.group.id, imageUrl).then(function(response) {
console.log('[Cover Inspector]', response);
if (!(img instanceof HTMLImageElement)) img = document.body.querySelector('div#covers img');
updateImage(imageUrl, torrentGroup, img);
}))).catch(function(reason) {
if (!torrentGroup.group.wikiImage && !inCoversCollage('missing', torrentGroup))
addToCoversCollage('missing', torrentGroup.group.id);
if (torrentGroup.group.wikiImage && !inCoversCollage('invalid', torrentGroup))
ihh.verifyImageUrl(torrentGroup.group.wikiImage).catch(reason =>
{ addToCoversCollage('invalid', torrentGroup.group.id) });
return Promise.reject(reason);
})));
}
function setFocus(elem) {
if (!(elem instanceof HTMLElement)) throw 'Invalid argument';
elem.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
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 = elem.onmouseleave = function(evt) {
if (evt.relatedTarget == evt.currentTarget) return false;
evt.currentTarget.style.color = evt.type == 'mouseenter' ? 'orange' : 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 failhandler(reason) {
if (this && this.logFail) this.logFail(`groupId ${groupId} cover lookup failed: ${reason}`);
console.log('[Cover Inspector] groupId', groupId, 'cover lookup failed:', reason);
return reason;
});
}
}
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 = ' groups 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;
setFocus(parent.parentNode.parentNode);
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;
Promise.all(inspectWorkers).then(statuses => !noEditPerms && ajaxApiKey && !readOnly && !noBatchProcessing ? imageHostHelper.then(function(ihh) {
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('unpreferred-host') && minimumRehostAge > 0) {
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 || !checkSavedRecovery()) return false;
setFocus(parent.parentNode.parentNode);
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('No editing permissions')).catch(reason => { currentTarget.remove() });
}, 'inspect-all-covers');
if (!noEditPerms && ajaxApiKey && !readOnly && !noBatchProcessing && !noAutoLookups) imageHostHelper.then(function(ihh) {
function setCoverFromTorrentGroup(torrentGroup, img, reason) {
if (!torrentGroup) throw 'Invalid argument';
return coverLookup(torrentGroup, ihh).then(imageUrls =>
ihh.rehostImageLinks([imageUrls[0]]).then(ihh.singleImageGetter).then(imageUrl =>
setGroupImage(torrentGroup.group.id, imageUrl, autoLookupSummary(reason)).then(function(response) {
console.log('[Cover Inspector]', response);
updateImage(imageUrl, torrentGroup, img);
if (autoOpenSucceed) openGroup(torrentGroup);
return imageUrl;
}))).catch(function(reason) {
if (!torrentGroup.group.wikiImage && !inCoversCollage('missing', torrentGroup))
addToCoversCollage('missing', torrentGroup.group.id);
if (torrentGroup.group.wikiImage && !inCoversCollage('invalid', torrentGroup))
ihh.verifyImageUrl(torrentGroup.group.wikiImage)
.catch(reason => { addToCoversCollage('invalid', 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);
console.log('[Cover Inspector] Links found in torrent descriptions for', torrentGroup, ':', urls);
});
return Promise.reject(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 (!checkSavedRecovery()) return false;
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');
setFocus(parent.parentNode.parentNode);
iterateReleaseGroups(function(groupId, img, failHandler) {
if (img instanceof HTMLImageElement) {
if (!hasArtworkSet(img)) queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
setCoverFromTorrentGroup(torrentGroup, img, 'missing')).catch(failHandler.bind(ihh))
.then(status => { counterDecrement('missing-covers-countdown', index) });
} else queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
torrentGroup.group.wikiImage ? true : setCoverFromTorrentGroup(torrentGroup, null, 'missing'))
.catch(failHandler.bind(ihh)).then(status =>
{ if (status != true) counterDecrement('missing-covers-countdown', index) });
});
}, 'covers-auto-lookup', missingImages.length > 0 ? (missingImages.length + ' covers missing') : undefined);
addHeaderButton('Fix invalid covers', function autoFix(evt) {
if (!checkSavedRecovery()) return false;
if (evt.currentTarget.count > 0) changeToCounter(evt.currentTarget, 'invalid-covers-countdown');
else evt.currentTarget.remove();
const autoAdd = parent.querySelector('span.covers-auto-lookup');
if (autoAdd != null) autoAdd.remove();
setFocus(parent.parentNode.parentNode);
iterateReleaseGroups(function(groupId, img, failHandler) {
function validateImage(imageUrl) {
if (!httpParser.test(imageUrl)) return Promise.reject('unset or invalid format');
const deproxiedSrc = deProxifyImgSrc(imageUrl);
return (deproxiedSrc ? setGroupImage(groupId, deproxiedSrc, 'Deproxied release image (not working anymore)')
.then(result => ihh.verifyImageUrl(deproxiedSrc)): ihh.verifyImageUrl(imageUrl)).then(verifiedImageUrl => true);
}
if (img instanceof HTMLImageElement) validateImage(realImgSrc(img)).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).catch(failHandler.bind(ihh))
.then(status => { counterDecrement('invalid-covers-countdown', index) }));
}); else queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
validateImage(torrentGroup.group.wikiImage).catch(function(reason) {
console.log('[Cover Inspector] Invalid or missing cover for groupId %d, reason:', groupId, reason);
return setCoverFromTorrentGroup(torrentGroup, null, reason);
})).catch(failHandler.bind(ihh)).then(status =>
{ if (status != true) counterDecrement('invalid-covers-countdown', index) });
});
}, 'auto-fix-covers', 'Missing covers lookup included');
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);
if (images.length > 0 && GM_getValue('auto_inspect_cached', false) && Object.keys(imageDetailsCache).length > 0)
iterateReleaseGroups(function(groupId, img) {
if (img != null && hasArtworkSet(img) && realImgSrc(img) in imageDetailsCache) inspectImage(img, groupId);
});
}
const urlParams = new URLSearchParams(document.location.search), id = parseInt(urlParams.get('id')) || undefined;
const findParent = table => table instanceof HTMLElement
&& Array.prototype.find.call(table.querySelectorAll(':scope > tbody > tr:first-of-type > td'),
td => /^(?:Torrents?|Name)\b/.test(td.textContent.trim())) || null;
// 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('Process saved queue? (' + savedQueue.length + ' tabs to open)')) return;
for (let queuedEntry of savedQueue) openTabLimited(queuedEntry.endpoint, queuedEntry.params, queuedEntry.hash);
});
GM_registerMenuCommand('Load saved queue for later', function() {
if (confirm('Saved queue (' + savedQueue.length + ' tabs to open) will be prepended to current, continue?'))
tabsQueueRecovery = savedQueue.concat(tabsQueueRecovery);
});
}
} catch(e) { console.warn(e) }
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();
});
}
function resolveTorrentRow(tr, ihh) {
function setStatus(newStatus, ...addedText) {
let td = tr.querySelector('td.status');
console.assert(td != null); if (td == null) return; // assertion failed
if (typeof newStatus == 'number') status = newStatus; else if (status == undefined) status = 2;
td.className = 'status ' + td.textContent + ' status-code-' + status;
if (status != 2 && typeof td.flashTimer == 'number') {
clearInterval(td.flashTimer);
delete td.flashTimer;
td.style.transition = null;
}
td.style.color = ['red', 'orange', '#cc0', '#8a0', '#0a0'][status];
td.textContent = status > 2 ? 'success' : status < 2 ? 'failed' : 'resolving';
td.style.opacity = 1;
if (status == 2) {
td.style.transition = 'opacity 50ms';
if (typeof td.flashTimer != 'number') td.flashTimer = setInterval(elem =>
{ elem.style.opacity = elem.style.opacity < 1 ? 1 : 0.25 }, 500, td);
}
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'));
if (status <= 0) if (autoHideFailed) tr.hidden = true;
else if ((td = document.getElementById('hide-status-failed')) != null) td.hidden = false;
}
const groupId = getGroupId(tr), tooltips = [ ];
let status;
if (!(groupId > 0)) return setStatus(0, 'Could not extract torrent id');
const autoHideFailed = GM_getValue('auto_hide_failed', false);
queryAjaxAPI('torrentgroup', { id: groupId }).then(function(torrentGroup) {
function removeFromCollages(...collageIndexes) {
for (let collageIndex of collageIndexes) if (inCoversCollage(collageIndex, torrentGroup))
removeFromCoversCollage(collageIndex, torrentGroup).then(() =>
{ setStatus(status, `(removed from all ${collageIndex} covers collages)`) });
}
const qualityEmphasisCategory = [1].includes(torrentGroup.group.categoryId);
const isCollagePage = id > 0 && ['/collages.php', '/collage.php'].includes(document.location.pathname);
const isMissingCoverCollage = isCollagePage && coverRelatedCollages && ['missing', 'invalid', 'investigate']
.some(index => Array.isArray(coverRelatedCollages[index]) && coverRelatedCollages[index].includes(id));
const isPoorCoverCollage = isCollagePage && coverRelatedCollages && ['poor'].some(index =>
Array.isArray(coverRelatedCollages[index]) && coverRelatedCollages[index].includes(id));
let q0 = -1;
return (function() {
if (!torrentGroup.group.wikiImage) return Promise.reject('none set');
const deproxiedSrc = deProxifyImgSrc(torrentGroup.group.wikiImage);
return deproxiedSrc ? setGroupImage(torrentGroup.group.id, deproxiedSrc, 'Deproxied release image (not working anymore)')
.then(result => ihh.verifyImageUrl(deproxiedSrc)) : ihh.verifyImageUrl(torrentGroup.group.wikiImage);
})().then(function(imageUrl) {
const hostname = new URL(imageUrl).hostname, domain = hostname.split('.').slice(-2).join('.');
return !isOnDomainList(hostname, 2) ? testImageQuality(imageUrl).then(function(q) {
if ((q0 = q) < (qualityEmphasisCategory ? 2 : 1))
return Promise.reject('low resolution of existing image');
setStatus(4, 'This release group seems to have a valid image');
removeFromCollages('missing', 'invalid', 'investigate');
if (!isPoorCoverCollage) removeFromCollages('poor');
else if ('autoOpenSucceed') openGroup(torrentGroup);
if (Array.isArray(preferredHosts) && preferredHosts.includes(hostname)
|| isOnDomainList(imageUrl.hostname, 0)/* || !isOnDomainList(imageUrl.hostname, 1)*/)
return null;
return 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);
return imageUrl;
})).catch(function(reason) {
setStatus(status, 'Cover rehost failed: ' + reason);
console.log('[Cover Inspector]', reason);
return null;
});
}) : Promise.reject('Release group having image at bad host');
}).catch(reason => (setStatus(2, `Looking for a new cover image (${reason})`),
coverLookup(torrentGroup, ihh, isPoorCoverCollage)).then(imageUrls => testImageQuality(imageUrls[0]).then(function(q) {
if (q <= q0 || q < (/*qualityEmphasisCategory ? 2 : */1) && isPoorCoverCollage)
return Promise.reject('Image found is poor quality (resolution)');
return ihh.rehostImageLinks(imageUrls, true, false, true)
.then(results => results.map(ihh.directLinkGetter), function(reason) {
setStatus(status, 'Cover rehost failed: ' + reason);
return imageUrls;
}).then(imageUrls => setGroupImage(torrentGroup.group.id, imageUrls[0], autoLookupSummary(reason)).then(function(response) {
setStatus(4, response);
console.log('[Cover Inspector]', response);
removeFromCollages('missing', 'invalid', 'investigate');
if (q < 1 || (q < 2 && qualityEmphasisCategory)) {
if (!inCoversCollage('poor', torrentGroup)) addToCoversCollage('poor', torrentGroup).then(_status =>
{ setStatus(status, '(added to poor quality covers collage)') });
setStatus(3, 'However the quality is poor (resolution)');
} else removeFromCollages('poor');
if (imageUrls.length > 1) setStatus(3, '(more external links require attention)');
if (autoOpenSucceed) openGroup(torrentGroup);
return imageUrls[0];
}));
}))).catch(function(reason) {
if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject(reason);
return 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, reason, '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);
console.log('[Cover Inspector] Links found in torrent descriptions for', torrentGroup, ':', urls);
});
});
}).catch(reason => { setStatus(0, reason) });
}
switch (document.location.pathname) {
case '/artist.php':
if (id > 0) {
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) {
function embedCSESearch(gridSize = 178) {
function killButton(color = '#0a0', retVal = false) {
if (button != null) {
if (color) button.style.color = color;
setTimeout(function(elem) {
elem.style.opacity = 0;
if (elem.parentNode.parentNode != null) setTimeout(elem => { elem.remove() }, 2000, elem.parentNode);
}, 5000, button);
}
return retVal;
}
console.assert(album.title, album);
const button = document.body.querySelector('span.cse-search > a');
if (button != null) if (button.disabled) return false; else button.disabled = true;
if (!album.title || document.body.querySelector('div.main_column > div.box.cse-search-results') != null)
return killButton('red');
let anchor = document.body.querySelector('div.main_column > table.torrent_table');
if (anchor == null) return killButton('red');
const getArtists = (importance, maxArtists = 2) =>
importance in album.artists && album.artists[importance].slice(0, maxArtists).join(' & ') || undefined;
if (album.releaseType != 'Compilation') var artist = getArtists('artists_dj') || getArtists('artist_main');
if (!artist && ['Compilation', 'DJ Mix'].includes(album.releaseType)) artist = 'Various';
[button.style.color, button.style.cursor, button.textContent] = ['orange', 'progress', 'Waiting...'];
const animation = button.parentNode.animate([
{ offset: 0.0, opacity: 1 },
{ offset: 0.4, opacity: 1 },
{ offset: 0.5, opacity: 0.1 },
{ offset: 0.9, opacity: 0.1 },
], { duration: 600, iterations: Infinity });
setTooltip(button);
cseSearch(bareReleaseTitle(album.title), artist).then(function(results) {
//console.debug('CSE Results:', results);
const [box, body] = ['DIV', 'DIV'].map(Document.prototype.createElement.bind(document));
box.className = 'box cse-search-results';
box.innerHTML = '<div class="head"><a href="#">↑</a> <strong>Cover Search Engine search results</strong></div>';
body.className = 'body';
body.style = `
padding: 15px; width: 100%; max-height: 90vh; box-sizing: border-box;
display: grid; gap: 5px; grid-template-columns: repeat(3, auto);
overflow-y: auto; scrollbar-gutter: auto;
`;
results.forEach(function(result, index) {
function addLabelInfo(caption, value) {
if (!caption || !value) return;
const [entry, b] = ['SPAN', 'SPAN'].map(Document.prototype.createElement.bind(document));
entry.style = 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
entry.className = 'detail';
if (caption.toLowerCase().startsWith('<svg')) {
b.innerHTML = caption;
b.firstElementChild.setAttribute('fill', 'white');
b.firstElementChild.setAttribute('height', '1em');
b.firstElementChild.style.marginBottom = '-1pt';
b.style = 'display: inline-block; min-width: 1.4em;';
entry.append(b);
} else {
b.textContent = caption;
b.style = 'font-weight: bold;';
entry.append(b, ': ');
}
entry.append(value);
label.append(entry);
}
const [item, img, label] = ['DIV', 'IMG', 'DIV'].map(Document.prototype.createElement.bind(document));
item.className = 'cse-search-result';
item.dataset.result = JSON.stringify(result);
item.dataset.imageUrl = result.bigCoverUrl || result.smallCoverUrl;
item.onmouseenter = item.onmouseleave = function(evt) {
if (evt.relatedTarget == evt.currentTarget) return false;
evt.currentTarget.lastElementChild.style.opacity = evt.type == 'mouseenter' ? 1 : 0;
evt.currentTarget.style.boxShadow = evt.type == 'mouseenter' ? '0 0 5px 5px yellow' : 'none';
evt.currentTarget.style.zIndex = evt.type == 'mouseenter' ? 2 : 'auto';
//evt.currentTarget.style.transform = evt.type == 'mouseenter' ? 'scale(1.05)' : 'none';
};
item.style = `position: relative; width: ${gridSize}px; height: ${gridSize}px; cursor: pointer; transition: box-shadow, transform 200ms;`;
item.draggable = true;
img.className = 'cse-search-preview';
img.loading = 'eager'; img.referrerPolicy = 'same-origin';
img.src = result.smallCoverUrl || result.bigCoverUrl;
img.width = img.height = gridSize;
label.className = 'cse-search-result-label';
label.draggable = true;
label.style = `
position: absolute; bottom: 0; left: 0; width: 100%; padding: 5px; box-sizing: border-box;
display: flex; flex-flow: column;
color: white; background-color: #0008; font: normal 7pt "Noto Sans", sans-serif;
opacity: 0; transition: opacity 200ms;
overflow: hidden; text-overflow: ellipsis;
`;
addLabelInfo('<svg viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000"><path d="M500,990c-251.9,0-456.2-82.2-456.2-183.7l0,0V702.9c0,101.5,204.3,164.6,456.2,164.6c251.9,0,456.2-63.1,456.2-164.6v103.4l0,0C956.2,907.8,751.9,990,500,990L500,990z M500,806.3c-251.9,0-456.2-82.3-456.2-183.8V519.1c0,101.5,204.3,164.6,456.2,164.6c251.9,0,456.2-63.1,456.2-164.6v103.4C956.2,724,751.9,806.3,500,806.3L500,806.3z M500,622.5c-251.9,0-456.2-82.3-456.2-183.8V335.4C43.8,436.9,248.1,500,500,500c251.9,0,456.2-63.1,456.2-164.6v103.4C956.2,540.2,751.9,622.5,500,622.5L500,622.5z M500,438.8c-251.9,0-456.2-82.3-456.2-183.8v-61.3C43.8,92.3,248.1,10,500,10c251.9,0,456.2,82.3,456.2,183.8V255C956.2,356.5,751.9,438.8,500,438.8L500,438.8z"/></svg>', result.source.toUpperCase());
if (result.releaseInfo) {
item.dataset.url = result.releaseInfo.url;
addLabelInfo('<svg viewBox="0 0 512 512"><path d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm256 32a32 32 0 1 1 0-64 32 32 0 1 1 0 64zm-96-32a96 96 0 1 0 192 0 96 96 0 1 0 -192 0zM96 240c0-35 17.5-71.1 45.2-98.8S205 96 240 96c8.8 0 16-7.2 16-16s-7.2-16-16-16c-45.4 0-89.2 22.3-121.5 54.5S64 194.6 64 240c0 8.8 7.2 16 16 16s16-7.2 16-16z"></path></svg>', result.releaseInfo.title);
addLabelInfo('<svg viewBox="0 0 448 512"><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"></path></svg>', result.releaseInfo.artist);
addLabelInfo('<svg viewBox="0 0 448 512"><path d="M96 32V64H48C21.5 64 0 85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 48-48V192z"></path></svg>', result.releaseInfo.date);
if (result.releaseInfo.tracks > 0)
addLabelInfo('<svg viewBox="0 0 512 512"><path d="M499.1 6.3c8.1 6 12.9 15.6 12.9 25.7v72V368c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V147L192 223.8V432c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V200 128c0-14.1 9.3-26.6 22.8-30.7l320-96c9.7-2.9 20.2-1.1 28.3 5z"></path></svg>', `${result.releaseInfo.tracks} ${result.releaseInfo.tracks > 1 ? 'tracks' : 'track'}`);
addLabelInfo('<svg viewBox="0 0 576 512"><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm80 256h64c44.2 0 80 35.8 80 80c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16c0-44.2 35.8-80 80-80zm-32-96a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zm256-32H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H368c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H368c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H368c-8.8 0-16-7.2-16-16s7.2-16 16-16z"></path></svg>', result.releaseInfo.catalog);
addLabelInfo('<svg viewBox="0 0 512 512"><path d="M24 32C10.7 32 0 42.7 0 56V456c0 13.3 10.7 24 24 24H40c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24H24zm88 0c-8.8 0-16 7.2-16 16V464c0 8.8 7.2 16 16 16s16-7.2 16-16V48c0-8.8-7.2-16-16-16zm72 0c-13.3 0-24 10.7-24 24V456c0 13.3 10.7 24 24 24h16c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24H184zm96 0c-13.3 0-24 10.7-24 24V456c0 13.3 10.7 24 24 24h16c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24H280zM448 56V456c0 13.3 10.7 24 24 24h16c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24H472c-13.3 0-24 10.7-24 24zm-64-8V464c0 8.8 7.2 16 16 16s16-7.2 16-16V48c0-8.8-7.2-16-16-16s-16 7.2-16 16z"></path></svg>', result.releaseInfo.barcode);
}
getImageDetails(item.dataset.imageUrl).then(function(imageDetails) {
addLabelInfo('<svg viewBox="0 0 62.89 62.89"><path d="M3.88 0l55.13 0c2.14,0 3.88,1.75 3.88,3.88l0 55.13c0,2.14 -1.74,3.88 -3.88,3.88l-55.13 0c-2.13,0 -3.88,-1.74 -3.88,-3.88l0 -55.13c0,-2.13 1.75,-3.88 3.88,-3.88zm50.61 52.62l0 0 0 -5.6 0 -18.67c0,-1.02 -0.84,-1.86 -1.87,-1.86l-5.6 0c-1.02,0 -1.86,0.84 -1.86,1.86l0 10.21 -20.82 -20.82 10.2 0c1.03,0 1.87,-0.84 1.87,-1.87l0 -5.6c0,-1.03 -0.84,-1.87 -1.87,-1.87l-18.67 0 -5.6 0 0 0 -0.05 0 0 0 -0.05 0 -0.04 0.01 0 0 -0.05 0 0 0 -0.05 0.01 0 0 -0.04 0 -0.05 0.01 0 0 -0.05 0.01 -0.04 0.01 0 0 -0.05 0.01 -0.04 0.01 -0.05 0.02 0 0 -0.04 0.01 0 0 -0.04 0.02 0 0 -0.04 0.01 0 0 -0.05 0.02 -0.04 0.02 -0.04 0.02 0 0 -0.04 0.02 0 0 -0.04 0.02 0 0 -0.04 0.02 0 0 -0.04 0.02 -0.04 0.03 -0.03 0.02 -0.04 0.03 -0.04 0.02 0 0 -0.03 0.03 -0.04 0.03 0 0 -0.03 0.03 -0.04 0.03 -0.03 0.03 0 0 -0.03 0.03 0 0 -0.03 0.03 0 0 -0.03 0.03 -0.03 0.04 -0.03 0.03 0 0 -0.03 0.04 -0.03 0.03 0 0 -0.02 0.04 -0.03 0.04 -0.02 0.03 -0.03 0.04 -0.02 0.04 0 0 -0.02 0.04 0 0 -0.02 0.04 0 0 -0.02 0.04 0 0 -0.02 0.04 -0.02 0.04 -0.02 0.05 0 0 -0.01 0.04 0 0 -0.02 0.04 0 0 -0.01 0.04 0 0 -0.02 0.05 -0.01 0.04 -0.01 0.05 0 0 -0.01 0.04 -0.01 0.05 0 0 -0.01 0.05 0 0.04 0 0 -0.01 0.05 0 0 0 0.05 0 0 -0.01 0.04 0 0.05 0 0 0 0.05 0 0 0 5.6 0 18.67c0,1.03 0.84,1.87 1.87,1.87l5.6 0c1.03,0 1.87,-0.84 1.87,-1.87l0 -10.2 20.82 20.82 -10.21 0c-1.02,0 -1.86,0.84 -1.86,1.86l0 5.6c0,1.03 0.84,1.87 1.86,1.87l18.67 0 5.6 0 0 0 0.05 0 0 0 0.05 0 0.05 0 0 0 0.05 -0.01 0 0 0.04 0 0 0 0.05 -0.01 0.04 -0.01 0 0 0.05 -0.01 0.05 -0.01 0 0 0.04 -0.01 0.04 -0.01 0.05 -0.01 0 0 0.04 -0.02 0 0 0.05 -0.01 0 0 0.04 -0.02 0 0 0.04 -0.02 0.04 -0.01 0.04 -0.02 0 0 0.04 -0.02 0 0 0.04 -0.02 0 0 0.04 -0.03 0 0 0.04 -0.02 0.04 -0.02 0.04 -0.03 0.03 -0.02 0.04 -0.03 0 0 0.04 -0.03 0.03 -0.03 0 0 0.03 -0.03 0.04 -0.03 0.03 -0.03 0 0 0.03 -0.03 0 0 0.03 -0.03 0 0 0.04 -0.03 0.03 -0.04 0.02 -0.03 0 0 0.03 -0.03 0.03 -0.04 0 0 0.03 -0.04 0.02 -0.03 0.03 -0.04 0.02 -0.04 0.02 -0.04 0 0 0.03 -0.04 0 0 0.02 -0.04 0 0 0.02 -0.04 0 0 0.02 -0.04 0.01 -0.04 0.02 -0.04 0 0 0.02 -0.05 0 0 0.01 -0.04 0 0 0.02 -0.04 0 0 0.01 -0.05 0.01 -0.04 0.01 -0.05 0 0 0.01 -0.04 0.01 -0.05 0 0 0.01 -0.04 0.01 -0.05 0 0 0 -0.05 0 0 0.01 -0.04 0 0 0 -0.05 0 -0.05 0 0 0 -0.05z"/></svg>', imageDetails.width + '×' + imageDetails.height);
addLabelInfo('<svg viewBox="0 0 20 20"><path d="M17 12v5H3v-5H1v5a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5z"/><path d="M10 15l5-6h-4V1H9v8H5l5 6z"/></svg>', formattedSize(imageDetails.size));
});
if (label.childElementCount > 0) item.append(img, label);
item.onclick = function(evt) {
const target = evt.currentTarget;
(!evt.ctrlKey && !noEditPerms && ajaxApiKey ? imageHostHelper.then(function(ihh) {
const img = document.body.querySelector('div#covers img');
ihh.verifyImageUrl(target.dataset.imageUrl).then(function(imageUrl) {
if (!confirm('Current cover is going to be replaced by\n' + imageUrl)) return false;
if (img != null) img.style.opacity = 0.3;
return ihh.rehostImageLinks([imageUrl]).then(ihh.singleImageGetter) .then(imageUrl =>
setGroupImage(id, imageUrl, 'Non-automated cover update from external source').then(function(response) {
console.log(response);
if (img != null) {
setNewSrc(img, imageUrl);
inspectImage(img, id).then(status => { updateCoverCollages(status, id) });
} else document.location.reload();
}));
}).catch(function(reason) {
ihh.logFail('Setting cover from result url failed: ' + reason);
if (img != null && img.style.opacity < 1) img.style.opacity = 1;
});
}) : Promise.reject('Site edit not available')).catch(reason =>
{ GM_openInTab(target.dataset[evt.shiftKey ? 'imageUrl' : 'url'], false) });
};
item.onauxclick = function(evt) {
if (evt.button != 1) return true;
GM_openInTab(evt.currentTarget.dataset.url, false)
return false;
};
(!noEditPerms && ajaxApiKey && !readOnly ? imageHostHelper.then(function() {
item.title = `Use simple click to set album cover from this source
Use Ctrl+click to open source page in new window
Use Ctrl+Shift+click to open cover image in new window
Drag anywhere to drop full resolution direct link
` + item.dataset.imageUrl;
}) : Promise.reject('Site edit not available')).catch(function() {
item.title = `Use simple click to open source page in new window
Use Shift+click to open cover image in new window
Drag anywhere to drop full resolution direct link
` + item.dataset.imageUrl;
});
item.ondragstart = function(evt) {
if (!evt.dataTransfer) return;
evt.dataTransfer.clearData();
evt.dataTransfer.setData('text/uri-list', evt.currentTarget.dataset.imageUrl);
evt.dataTransfer.setData('text/plain', evt.currentTarget.dataset.imageUrl);
evt.dataTransfer.setData('text/imageUrl', evt.currentTarget.dataset.imageUrl);
if (item.dataset.url) evt.dataTransfer.setData('text/url', evt.currentTarget.dataset.url);
// GM_xmlhttpRequest({ method: 'GET', url: evt.currentTarget.dataset.imageUrl, responseType: 'blob',
// onload: function(response) {
// if (response.status < 200 || response.status >= 400) return;
// draggedImage = new File([response.response], 'cover.jpg');
// },
// context: evt.dataTransfer,
// });
};
body.append(item);
});
box.append(body);
anchor.after(box);
[button.textContent, button.style.cursor] = ['Success', 'default'];
killButton('#0a0');
setFocus(box);
if (cseSearchMenu) GM_unregisterMenuCommand(cseSearchMenu);
}).catch(function(reason) {
[button.textContent, button.style.cursor, button.style.color, button.disabled] =
['Error', null, 'red', false];
setTooltip(button, reason);
//killButton('red');
}).then(() => { if (animation) animation.cancel() });
return true;
}
if (document.body.querySelector('div#content div.sidebar > div.box.box_artists') != null) {
var album = { title: document.body.querySelector('div#content div.header > h2'), artists: { } };
if (album.title != null && (album.releaseType = /\[(\d+)\] \[(.+)\]/.exec(album.title.lastChild.textContent.trim())) != null) Object.assign(album, {
title: album.title.lastElementChild.textContent.trim(),
year: parseInt(album.releaseType[1]),
releaseType: album.releaseType[2],
});
for (let a of document.body.querySelectorAll('ul#artist_list > li > a[dir="ltr"]')) {
if (!(a.parentNode.className in album.artists)) album.artists[a.parentNode.className] = [ ];
album.artists[a.parentNode.className].push(a.textContent.trim());
}
console.debug('Music album:', album);
}
if (typeof GM_getTabs == 'function') GM_getTabs(function(tabs) {
for (let tab in tabs) if ((tab = tabs[tab]) && 'torrentGroups' in tab) try {
if (!(tab = tab.torrentGroups[id])) continue;
console.info('Torrent group %d found in tabs data', id);
unsafeWindow.torrentGroup = tab;
unsafeWindow.dispatchEvent(Object.assign(new Event('torrentGroup'), { data: tab }));
} catch (e) { console.warn(e) }
});
for (let img of document.body.querySelectorAll('div#covers img')) inspectImage(img, id);
if (!noEditPerms && ajaxApiKey && !readOnly) 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]).then(ihh.singleImageGetter)
.then(imageUrl => setGroupImage(id, imageUrl, 'Cover update from description link').then(function(response) {
console.log(response);
if (img != null) {
setNewSrc(img, imageUrl);
inspectImage(img, id).then(status => { updateCoverCollages(status, 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;
const tooltip = 'Alt + click to set release cover from this URL (or use context menu command)';
a.title = tooltip;
if (GM_getValue('tooltip_desc_links_image', false) && typeof jQuery.fn.tooltipster == 'function') imageHostHelper.then(function(ihh) {
ihh.imageUrlResolver(a.href).then(singleResultGetter).then(linkImage => { $(a).tooltipster({
content: '<img src="' + linkImage + '" width="225" referrerpolicy="same-origin" /><div style="margin-top: 5pt; max-width: 225px;">' + tooltip + '</div>',
}) });
});
}
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 (!noAutoLookups) GM_registerMenuCommand('Cover auto lookup', () => { findCover(id).catch(alert) }, 'A');
cseSearchMenu = GM_registerMenuCommand('Search cover (manual)', () => { embedCSESearch() }, 'M');
});
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();
}
function embedpage(url, title, className) {
if (!url || !title) throw 'Invalid argument';
const anchor = document.body.querySelector('div.main_column > div.torrent_description');
if (anchor == null) return null;
const [div, iframe] = ['DIV', 'IFRAME'].map(Document.prototype.createElement.bind(document));
div.classList.add('box');
if (className) div.classList.add(className);
div.innerHTML = '<div class="head"><a href="#">↑</a> <strong>' + title + '</strong></div><div class="body"></div>';
iframe.frameBorder = 0; iframe.allowFullscreen = true;
iframe.referrerPolicy = 'no-referrer';
iframe.sandbox.add('allow-scripts', 'allow-forms', 'allow-same-origin');
iframe.width = '100%'; iframe.height = '500';
iframe.src = url;
// iframe.onload = function(evt) {
// const document = evt.currentTarget.contentDocument || evt.currentTarget.contentWindow.document;
// if (document == null) return;
// };
iframe.onerror = evt => { anchor.removeChild(div) };
div.querySelector('div.body').append(iframe);
return anchor.before(div);
}
// Embed Cover Search Engine results
if (album) if (Boolean(urlParams.get('embed-cse-search'))) embedCSESearch(); else {
let anchor = document.getElementById('add_cover_div');
if (anchor != null || (anchor = document.querySelector('div.sidebar > div.box_image > div.head')) != null) {
const [span, a] = ['SPAN', 'A'].map(Document.prototype.createElement.bind(document));
span.className = 'cse-search';
span.style = 'float: right; margin-left: 5pt; font-weight: normal;';
a.href = '#';
a.textContent = anchor.id == 'add_cover_div' ? 'Search cover' : 'Search';
a.className = 'brackets';
a.style.transition = 'opacity 2s';
a.onclick = function(evt) {
evt.stopImmediatePropagation();
embedCSESearch();
return false;
};
span.append(a); anchor.firstElementChild.prepend(span);
}
}
// Embed Google Image search
if (album && Boolean(urlParams.get('embed-google-image-search')) && album.title && album.year > 0) {
let query = '"' + bareReleaseTitle(album.title) + '" ' + album.year;
const stringifyArtists = (importance, maxArtists = 3) => importance in album.artists ?
album.artists[importance].slice(0, maxArtists).map(artist => '"' + artist + '"').join(' ') : undefined;
if (album.releaseType != 'Compilation') {
if ('artists_dj' in album.artists) query = stringifyArtists('artists_dj') + ' ' + query;
else if ('artist_main' in album.artists) query = stringifyArtists('artist_main') + ' ' + query;
} else query = (stringifyArtists('artists_dj') || '"Various Artists"') + ' ' + query;
const embedUrl = new URL('https://www.google.com/search');
embedUrl.searchParams.set('q', query);
embedUrl.searchParams.set('tbm', 'isch');
//embedUrl.hash = 'islmp';
embedpage(embedUrl, 'Google image search results', 'google-image-search-results');
}
// Embed first description link if available
if (Boolean(urlParams.get('embed-desc-link-source'))) {
const links = getLinks(document.querySelector('div.torrent_description > div.body'));
if (links != null) for (let link of links) embedpage(link, 'Description Link Preview', 'desc-link-preview');
}
// Embed description links image previews
if (Boolean(urlParams.get('desc-links-image-preview'))) {
const anchor = document.body.querySelector('div.sidebar > div.box_image');
const links = getLinks(document.querySelector('div.torrent_description > div.body'));
if (anchor != null && links != null) imageHostHelper.then(function(ihh) {
let previewBox = document.createElement('DIV');
previewBox.className = 'box description_link_image_preview';
previewBox.innerHTML = '<div class="head"><strong>Description links image preview</strong></div><div class="pad"></div>';
anchor.after(previewBox);
previewBox = previewBox.querySelector('div.pad');
previewBox.style = 'display: flex; flex-direction: column; gap: 5pt;';
links.forEach(function(link, index) {
const div = document.createElement('DIV');
div.textContent = 'Resolving image...';
div.dataset.url = link; div.dataset.index = index;
previewBox.append(div);
ihh.imageUrlResolver(link).then(singleResultGetter).then(function(linkImage) {
const img = document.createElement('IMG');
img.onload = function(evt) {
evt.currentTarget.title = link + ' → ' + linkImage;
evt.currentTarget.alt = linkImage;
while (div.lastChild != null) div.removeChild(div.lastChild);
div.append(evt.currentTarget);
};
img.onerror = function(evt) {
div.textContent = 'Image loading error for ' + link;
div.style.color = 'red';
};
img.width = 225; img.referrerPolicy = 'same-origin'; img.src = linkImage;
}, function(reason) {
div.textContent = 'No valid image for ' + link;
div.style.color = 'red';
div.title = reason;
});
});
});
}
if (Boolean(urlParams.get('highlight-cover-collages')) && coverRelatedCollages)
for (let a of document.querySelectorAll('table.collage_table > tbody > tr > td:first-of-type > a')) {
const collageId = a.pathname == '/collages.php' && parseInt(new URLSearchParams(a.search).get('id'));
if (collageId > 0 && Object.keys(coverRelatedCollages).some(key =>
Array.isArray(coverRelatedCollages[key]) && coverRelatedCollages[key].includes(collageId))) {
a.style = 'color: white; font-weight: bold;';
a.parentNode.parentNode.style = 'color: white; background-color: darkorange;';
}
}
} else { // not torrent group
const useIndexes = urlParams.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':
if (!noEditPerms && ajaxApiKey && !readOnly && !noAutoLookups && !noBatchProcessing && coverRelatedCollages
&& Object.values(coverRelatedCollages).some(collageIds =>
Array.isArray(collageIds) && collageIds.includes(id))) imageHostHelper.then(function(ihh) {
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('Cover art auto lookup', function(evt) {
if (checkSavedRecovery()) evt.currentTarget.remove(); else return false;
const pager = document.body.querySelector('div.main_column > div.linkbox:not(.pager)');
if (pager != null) setFocus(pager);
const autoHideFailed = GM_getValue('auto_hide_failed', false);
document.body.querySelectorAll('table#discog_table > tbody > tr').forEach(function(tr) {
const td = document.createElement('TD');
tr.append(td);
if (tr.classList.contains('colhead_dark')) {
td.textContent = 'Status';
td.title = 'Result of attempt to add missing/broken/poor cover\nHover the mouse over status for more details'; //setTooltip(td, tooltip);
} else if (/^group_(\d+)$/.test(tr.id)) {
td.className = 'status';
td.style.opacity = 0.3;
td.textContent = 'unknown';
resolveTorrentRow(tr, ihh);
}
});
}, 'covers-auto-lookup', 'gold');
addButton('Hide failed', function(evt) {
evt.currentTarget.hidden = true;
for (let td of document.body.querySelectorAll('table#discog_table > tbody > tr[id] > td.status.status-code-0'))
td.parentNode.hidden = true;
}, 'hide-status-failed', undefined, false);
}
});
break;
case '/userhistory.php':
case '/top10.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;
case '/better.php':
if (!noEditPerms && ajaxApiKey && !readOnly && !noBatchProcessing && !noAutoLookups
&& ['artwork'].includes(urlParams.get('method'))) imageHostHelper.then(function(ihh) {
const linkBox = document.body.querySelector('div.header > div.linkbox');
console.assert(linkBox != null);
if (linkBox == null) throw 'linkbox not found';
const a = document.createElement('A');
a.id = 'covers-auto-lookup';
a.className = 'brackets';
a.textContent = 'Cover art auto lookup';
a.href = '#';
a.onclick = function(evt) {
if (!checkSavedRecovery()) return false;
evt.currentTarget.previousSibling.remove();
evt.currentTarget.remove();
const pager = document.body.querySelector('div.linkbox.pager');
if (pager != null) setFocus(pager);
const div = document.createElement('DIV'), span = document.createElement('SPAN');
div.style = 'position: fixed; top: 10pt; right: 10pt; padding: 3pt; background-color: #2f4f4f8a; color: white; font-weight: 600; border-radius: 5pt; box-shadow: 2px 2px 3px black';
div.id = 'hide-status-failed';
div.hidden = true;
span.textContent = 'Hide failed';
span.style = 'display: inline-block; padding: 5pt; cursor: pointer; transition: color 250ms;';
span.onclick = function(evt) {
evt.currentTarget.parentNode.hidden = true;
for (let td of document.body.querySelectorAll('table.torrent_table > tbody > tr.torrent > td.status.status-code-0'))
td.parentNode.hidden = true;
};
span.onmouseenter = span.onmouseleave = function(evt) {
if (evt.relatedTarget == evt.currentTarget) return false;
evt.currentTarget.style.color = evt.type == 'mouseenter' ? 'orange' : 'white';
};
div.append(span);
document.body.append(div);
document.body.querySelectorAll('table.torrent_table > tbody > tr').forEach(function(tr) {
const td = document.createElement('TD');
tr.append(td);
if (!(tr.classList.contains('torrent'))) return;
td.className = 'status';
td.style.opacity = 0.3;
td.textContent = 'unknown';
resolveTorrentRow(tr, ihh);
});
return false;
};
linkBox.append(' ', a);
});
break;
}
}