// ==UserScript==
// @name [RED/OPS] Upload Assistant
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.385
// @description Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection or web link, offline and online release integrity check, tracklist format customization, featured artists extraction, classical works formatting, cover art fetching from store, checking for previous upload, form enhancements and more
// @author Anakunda
// @copyright 2019-21, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license GPL-3.0-or-later
// @icon data:image/png;base64,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
// @match https://redacted.ch/upload.php*
// @match https://redacted.ch/torrents.php?action=editgroup&*
// @match https://redacted.ch/torrents.php?action=edit&*
// @match https://redacted.ch/requests.php?action=new*
// @match https://redacted.ch/requests.php?action=edit*
// @match https://notwhat.cd/upload.php*
// @match https://notwhat.cd/torrents.php?action=editgroup&*
// @match https://notwhat.cd/torrents.php?action=edit&*
// @match https://notwhat.cd/requests.php?action=new*
// @match https://notwhat.cd/requests.php?action=edit*
// @match https://orpheus.network/upload.php*
// @match https://orpheus.network/torrents.php?action=editgroup&*
// @match https://orpheus.network/torrents.php?action=edit&*
// @match https://orpheus.network/requests.php?action=new*
// @match https://orpheus.network/requests.php?action=edit*
// @match https://dicmusic.club/upload.php*
// @match https://dicmusic.club/torrents.php?action=editgroup&*
// @match https://dicmusic.club/torrents.php?action=edit&*
// @match https://dicmusic.club/requests.php?action=new*
// @match https://dicmusic.club/requests.php?action=edit*
// @connect file://*
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM_info
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
// @require https://openuserjs.org/src/libs/Anakunda/bencode-min.js
// @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
// @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// @require https://openuserjs.org/src/libs/Anakunda/progressBars.min.js
// @require https://openuserjs.org/src/libs/Anakunda/imageHostUploader.min.js
// @require https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js
// @require https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.min.js
// @require https://openuserjs.org/src/libs/Anakunda/langCodes.min.js
// @require https://openuserjs.org/src/libs/Anakunda/libStringDistance.min.js
// @downloadURL none
// ==/UserScript==
'use strict';
let userAuth = document.body.querySelector('input[name="auth"]');
if (userAuth != null) userAuth = userAuth.value; else throw 'User auth could not be located';
const urlParams = new URLSearchParams(document.location.search),
action = urlParams.get('action'),
artistEdit = Boolean(action) && action.toLowerCase() == 'edit',
artistId = parseInt(urlParams.get('artistid') || urlParams.get('id'));
if (!(artistId > 0)) throw 'Assertion failed: could not extract artist id';
let userId = document.body.querySelector('li#nav_userinfo > a.username');
if (userId != null) {
userId = new URLSearchParams(userId.search);
userId = parseInt(userId.get('id')) || null;
}
const isRED = document.location.hostname == 'redacted.ch';
function hasStyleSheet(name) {
if (name) name = name.toLowerCase(); else throw 'Invalid argument';
const hrefRx = new RegExp('\\/' + name + '\\b', 'i');
if (document.styleSheets) for (let styleSheet of document.styleSheets)
if (styleSheet.title && styleSheet.title.toLowerCase() == name) return true;
else if (styleSheet.href && hrefRx.test(styleSheet.href)) return true;
return false;
}
const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
if (isLightTheme) console.log('Light Gazelle theme detected');
const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
if (isDarkTheme) console.log('Dark Gazelle theme detected');
const createElements = (...tagNames) => tagNames.map(Document.prototype.createElement.bind(document));
let siteBlacklist = GM_getValue('site_blacklist');
if (siteBlacklist == undefined) GM_setValue('site_blacklist', [
'www.myspace.com', 'myspace.com',
'www.facebook.com', 'm.facebook.com', 'facebook.com',
'www.twitter.com', 'twitter.com',
'www.instagram.com', 'instagram.com',
'www.vk.com', 'vk.com',
]);
function loadArtist() {
const siteArtistsCache = { }, artistlessGroups = new Set;
function decodeHTML(html) {
const textArea = document.createElement("textarea");
textArea.innerHTML = html;
return textArea.value;
}
function decodeArtistTitles(artist) {
if (!artist) throw 'Invalid argument';
if (artist.titlesDecoded) return;
//const label = `Decode release titles for ${artist.id}`;
//console.time(label);
if (/*!isRED && */Array.isArray(artist.torrentgroup)) for (let torrentGroup of artist.torrentgroup)
if (torrentGroup.groupName) torrentGroup.groupName = decodeHTML(torrentGroup.groupName);
if (/*!isRED && */Array.isArray(artist.requests)) for (let request of artist.requests)
if (request.title) request.title = decodeHTML(request.title);
//console.timeEnd(label);
artist.titlesDecoded = true;
}
const findArtistId = artistName => artistName ? localXHR('/artist.php?' + new URLSearchParams({
artistname: artistName,
}).toString(), { method: 'HEAD' }).then(function(xhr) {
const url = new URL(xhr.responseURL);
return url.pathname == '/artist.php' && parseInt(url.searchParams.get('id')) || Promise.reject('not found');
}) : Promise.reject('Invalid argument');
function getSiteArtist(artistNameOrId, decodeTitles = false) {
if (!artistNameOrId) throw 'Invalid argument';
const titleDecoder = artist => (decodeArtistTitles(artist), artist);
if (typeof artistNameOrId == 'number') {
if (!(artistNameOrId > 0)) return Promise.reject('Invalid argument');
let result = queryAjaxAPICached('artist', { id: artistNameOrId });
return decodeTitles ? result.then(titleDecoder) : result;
}
if (artistNameOrId > 0)
console.trace('[AAM] Warning: possible call of getSiteArtist(...) with stringified artist id', artistNameOrId);
const artistNameCaseless = artistNameOrId.toLowerCase();
const key = Object.keys(siteArtistsCache).find(artist => artist.toLowerCase() == artistNameCaseless);
if (key) return decodeTitles ? siteArtistsCache[key].then(titleDecoder) : siteArtistsCache[key];
const result = queryAjaxAPICached('artist', { artistname: artistNameOrId }).then(function(response) {
if (response.name.toLowerCase() == artistNameCaseless) return response;
return findArtistId(artistNameOrId).then(artistId => queryAjaxAPICached('artist', { id: artistId }));
}, reason => reason == 'not found' && artistNameOrId > 0 ?
queryAjaxAPICached('artist', { id: parseInt(artistNameOrId) }) : Promise.reject(reason));
siteArtistsCache[artistNameOrId] = result;
return decodeTitles ? result.then(titleDecoder) : result;
}
return getSiteArtist(artistId).then(function(artist) {
const isGenericArtist = name => ['Various Artists', 'Unknown Artist', 'Unknown Artist(s)'].includes(name);
const tagsExclusions = tag => !/^(?:freely\.available|staff\.picks|delete\.this\.tag|\d{4}s)$/i.test(tag);
if (artist.tags) artist.tags = new TagManager(...artist.tags.map(tag => tag.name).filter(tagsExclusions));
siteArtistsCache[artist.name] = artist;
const rdExtractor = /\(\s*writes\s+redirect\s+to\s+(\d+)\s*\)/i;
let activeElement = null;
function getAlias(li) {
console.assert(li instanceof HTMLLIElement, 'li instanceof HTMLLIElement', li);
if (!(li instanceof HTMLLIElement)) return;
if (typeof li.alias == 'object') return li.alias;
const alias = {
id: li.querySelector(':scope > span:nth-of-type(1)'),
name: li.querySelector(':scope > span:nth-of-type(2)'),
redirectId: rdExtractor.exec(li.textContent),
};
if (alias.id == null || alias.name == null || !(alias.id = parseInt(alias.id.textContent))
|| !(alias.name = alias.name.textContent)) return;
if (alias.redirectId != null) alias.redirectId = parseInt(alias.redirectId[1]); else delete alias.redirectId;
return alias;
}
const resolveArtistId = (artistIdOrName = artist.id) => artistIdOrName > 0 ? Promise.resolve(artistIdOrName)
: typeof artistIdOrName == 'string' ? findArtistId(artistIdOrName) : Promise.resolve(artist.id);
const resloveArtistName = artistIdOrName => artistIdOrName > 0 ? artistIdOrName == artist.id ? artist.name
: getSiteArtist(artistIdOrName).then(artist => artist.name) : Promise.resolve(artistIdOrName);
function findAlias(aliasIdOrName, resolveFinalAlias = false, document = window.document) {
const addForm = document.body.querySelector('form.add_form');
if (addForm == null) throw 'Invalid page structure';
for (let li of addForm.parentNode.parentNode.querySelectorAll('div.box > div > ul > li')) {
const alias = getAlias(li);
if (alias && (aliasIdOrName > 0 && alias.id == aliasIdOrName
|| typeof aliasIdOrName == 'string' && alias.name.toLowerCase() == aliasIdOrName.toLowerCase()))
return alias.redirectId > 0 && resolveFinalAlias ? findAlias(alias.redirectId, true, document) : alias;
}
return null;
}
const findArtistAlias = (aliasIdOrName, artistIdOrName, resolveFinalAlias = false) => (function() {
if ((!artistIdOrName || artistIdOrName < 0) && artistEdit) return Promise.resolve(window.document);
return resolveArtistId(artistIdOrName).then(artistId => localXHR('/artist.php?' + new URLSearchParams({
action: 'edit',
artistid: artistId,
}).toString()));
})().then(document => findAlias(aliasIdOrName, resolveFinalAlias, document)
|| Promise.reject('Alias id/name not defined for this artist'));
const resolveAliasId = (aliasIdOrName, artistIdOrName, resolveFinalAlias = false) =>
aliasIdOrName >= 0 ? Promise.resolve(aliasIdOrName) : typeof aliasIdOrName == 'string' ?
findArtistAlias(aliasIdOrName, artistIdOrName, resolveFinalAlias).then(alias => alias.id)
: Promise.reject('Invalid argument');
const addAlias = (name, redirectTo = 0, artistIdOrName) => resolveArtistId(artistIdOrName).then(artistId =>
resolveAliasId(redirectTo, artistIdOrName && artistId, true).then(redirectTo => localXHR('/artist.php', { responseType: null }, new URLSearchParams({
action: 'add_alias',
artistid: artistId,
name: name,
redirect: redirectTo > 0 ? redirectTo : 0,
auth: userAuth,
}))));
const deleteAlias = (aliasIdOrName, artistIdOrName) => resolveAliasId(aliasIdOrName, artistIdOrName)
.then(aliasId => localXHR('/artist.php?' + new URLSearchParams({
action: 'delete_alias',
aliasid: aliasId,
auth: userAuth,
}).toString())).then(function(document) {
if (!/^\s*(?:Error)\b/.test(document.head.textContent)) return true;
const box = document.body.querySelector('div#content div.box');
if (box != null) alert(`Alias "${aliasIdOrName}" deletion failed:\n\n${box.textContent.trim()}`);
return false;
});
const renameArtist = (newName, artistIdOrName = artist.id) => resolveArtistId(artistIdOrName)
.then(artistId => localXHR('/artist.php', { responseType: null }, new URLSearchParams({
action: 'rename',
artistid: artistId,
name: newName,
auth: userAuth,
})));
const addSimilarArtist = (relatedIdOrName, artistIdOrName = artist.id) => resolveArtistId(artistIdOrName)
.then(artistId => resloveArtistName(relatedIdOrName).then(artistName =>
localXHR('/artist.php', { responseType: null }, new URLSearchParams({
action: 'add_similar',
artistid: artistId,
artistname: artistName,
auth: userAuth,
}))));
const addSimilarArtists = (similarArtists, artistIdOrName = artist.id) => resolveArtistId(artistIdOrName)
.then(artistId => Promise.all(similarArtists
.filter((name1, ndx, arr) => arr.findIndex(name2 => name2.toLowerCase() == name1.toLowerCase()) == ndx)
.map(similarArtist => addSimilarArtist(similarArtist, artistIdOrName && artistId || undefined))));
const changeArtistId = (newArtistIdOrName, artistIdOrName = artist.id) =>
resolveArtistId(artistIdOrName).then(artistId => resolveArtistId(newArtistIdOrName)
.then(newArtistId => localXHR('/artist.php', { responseType: null }, new URLSearchParams({
action: 'change_artistid',
artistid: artistId,
newartistid: newArtistId,
confirm: 1,
auth: userAuth,
}))));
const editArtist = (image = artist.image, body = artist.body, summary, artistIdOrName = artist.id, editNotes) =>
(image && unsafeWindow.imageHostHelper ? unsafeWindow.imageHostHelper.rehostImageLinks([image], true, false, false)
.then(unsafeWindow.imageHostHelper.singleImageGetter, function(reason) {
console.warn(reason);
return image;
}) : Promise.resolve(image)).then(image => resolveArtistId(artistIdOrName).then(artistId =>
localXHR('/artist.php', { responseType: null }, new URLSearchParams({
action: 'edit',
artistid: artistId,
image: image || '',
body: body || '',
summary: summary || '',
artisteditnotes: editNotes || '',
auth: userAuth,
}))));
function addAliasToGroup(groupId, aliasName, importances) {
if (!(groupId > 0) || !aliasName || !Array.isArray(importances))
return Promise.resolve('One or more arguments invalid');
const payLoad = new URLSearchParams({
action: 'add_alias',
groupid: groupId,
auth: userAuth,
});
for (let importance of importances) {
payLoad.append('aliasname[]', aliasName);
payLoad.append('importance[]', importance);
}
return localXHR('/torrents.php', { responseType: null }, payLoad);
}
const deleteArtistFromGroup = (groupId, artistIdOrName = artist.id, importances) =>
groupId > 0 && Array.isArray(importances) ? resolveArtistId(artistIdOrName)
.then(artistId => Promise.all(importances.map(importance => localXHR('/torrents.php?' + new URLSearchParams({
action: 'delete_alias',
groupid: groupId,
artistid: artistId,
importance: importance,
auth: userAuth,
}).toString(), { responseType: null })))) : Promise.reject('One or more arguments invalid');
function gotoArtistPage(artistIdOrName = artist.id) {
resolveArtistId(artistIdOrName)
.then(artistId => { document.location.assign('/artist.php?id=' + artistId.toString()) });
}
function gotoArtistEditPage(artistIdOrName = artist.id) {
resolveArtistId(artistIdOrName).then(function(artistId) {
document.location.assign('/artist.php?' + new URLSearchParams({
action: 'edit',
artistid: artistId,
}).toString() + '#aliases');
});
}
const wait = param => new Promise(resolve => { setTimeout(param => { resolve(param) }, 200, param) });
const clearRecoveryInfo = () => { GM_deleteValue('damage_control') };
function hasRecoveryInfo() {
const recoveryInfo = GM_getValue('damage_control');
return recoveryInfo && recoveryInfo.artist.id == artist.id;
}
const sameArtistConfidence = GM_getValue('artist_matching_threshold', 0.90),
sameTitleConfidence = GM_getValue('title_matching_threshold', 0.90),
cacheSizeReserve = GM_getValue('cache_size_reserve', 1280);
const stripRlsSuffix = title => title.replace(/\s+(?:EP|E\.\s?P\.|\((?:EP|E\.\s?P\.|[Ss]ingle|[Ll]ive)\)|-\s+(?:EP|E\.\s?P\.|[Ss]ingle|[Ll]ive))$/, '');
const titleCmpNorm = title => stripRlsSuffix(title).replace(/[^\w\u0080-\uFFFF]/g, '').toLowerCase();
// Discogs querying
const dcToken = GM_getValue('discogs_token', 'fJGcklUZogHYsgHaIWtqWWcdChKvJhpNknDKFHFk'),
dcKey = GM_getValue('discogs_key'), dcSecret = GM_getValue('discogs_secret'),
dcApiRateControl = { }, dcRequestsCache = new Map, dcArtistReleasesCache = new Map,
dcEntriesCache = Object.assign.apply({ }, ['artists', 'releases', 'masters', 'labels', 'user']
.map(type => ({ [type]: new Map }))),
dcSearchSize = GM_getValue('discogs_artist_search_size', 100);
let dcMasterYears = new Map, dcLastCachedSuccess, dcQueriesCache;
//try { dcMasterYears = new Map(GM_getValue('discogs_master_years')) } catch(e) { }
const dcNameNormalizer = artist => artist.replace(/\s+/g, ' ').replace(/\s+\(\d+\)$/, '');
function queryDiscogsAPI(endPoint, params) {
if (endPoint) endPoint = new URL(endPoint, 'https://api.discogs.com');
else return Promise.reject('No endpoint provided');
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 (dcRequestsCache.has(cacheKey)) return dcRequestsCache.get(cacheKey);
if (!dcQueriesCache && 'discogsQueriesCache' in sessionStorage
&& (parseInt(sessionStorage.aamCachedArtistId) == artist.id
|| sessionStorage.discogsQueriesCache.length < cacheSizeReserve * 2**10)) try {
dcQueriesCache = new Map(JSON.parse(sessionStorage.getItem('discogsQueriesCache')));
} catch(e) { console.warn(e) }
if (!dcQueriesCache) dcQueriesCache = new Map;
if (dcQueriesCache.has(cacheKey)) return Promise.resolve(dcQueriesCache.get(cacheKey));
const authHeader = { };
if (dcKey && dcSecret) authHeader.Authorization = `Discogs key=${dcKey}, secret=${dcSecret}`;
else if (dcToken) authHeader.Authorization = `Discogs token=${dcToken}`;
else console.warn('Discogs API: no authentication credentials are configured, the functionality related to Discogs is limited');
let requestsMax = 60;
const worker = new Promise((resolve, reject) => (function request(retryCounter = 0) {
const postpone = () => { setTimeout(request, dcApiRateControl.timeFrameExpiry - now, retryCounter + 1) };
const now = Date.now();
if (!dcApiRateControl.timeFrameExpiry || now > dcApiRateControl.timeFrameExpiry) {
dcApiRateControl.timeFrameExpiry = now + 60 * 1000 + 500;
if (dcApiRateControl.requestDebt > 0) {
dcApiRateControl.requestCounter = Math.min(requestsMax, dcApiRateControl.requestDebt);
dcApiRateControl.requestDebt -= dcApiRateControl.requestCounter;
console.assert(dcApiRateControl.requestDebt >= 0, 'dcApiRateControl.requestDebt >= 0');
} else dcApiRateControl.requestCounter = 0;
}
if (++dcApiRateControl.requestCounter <= requestsMax) GM_xmlhttpRequest({ method: 'GET', url: endPoint,
responseType: 'json',
headers: Object.assign({
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}, authHeader),
onload: function(response) {
let requestsUsed = /^(?:x-discogs-ratelimit):\s*(\d+)\b/im.exec(response.responseHeaders);
if (requestsUsed != null && (requestsUsed = parseInt(requestsUsed[1])) > 0) requestsMax = requestsUsed;
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 - requestsMax, 0);
}
if (response.status >= 200 && response.status < 400) {
dcQueriesCache.set(cacheKey, response = response.response);
if (!domStorageLimitReached) {
const serialized = JSON.stringify(Array.from(dcQueriesCache));
try {
sessionStorage.setItem('discogsQueriesCache', serialized);
dcLastCachedSuccess = serialized;
} catch(e) {
console.warn(e, `(${serialized.length})`);
if (/\b(?:NS_ERROR_DOM_QUOTA_REACHED)\b/.test(e)
|| e instanceof DOMException && e.name == 'QuotaExceededError') {
domStorageLimitReached = true;
sessionStorage.setItem('discogsQueriesCache', dcLastCachedSuccess);
dcLastCachedSuccess = undefined;
}
}
sessionStorage.setItem('aamCachedArtistId', artist.id);
}
resolve(response);
} else {
if (response.status == 429/* && retryCounter < 25*/) {
console.warn(defaultErrorHandler(response), response.response.message, '(' + retryCounter + ')',
`Rate limit used: ${requestsUsed}/${requestsMax}`);
postpone();
} else reject(defaultErrorHandler(response));
}
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}); else postpone();
})());
dcRequestsCache.set(cacheKey, worker);
return worker;
}
function discogsRedirectHandler(reason, type, id, callback) {
if (!/^(?:HTTP error 404)\b/i.test(reason)) return Promise.reject(reason);
if (!type || !(id > 0) || typeof callback != 'function') throw 'Invalid argument(s)';
const rx = new RegExp(`\\/${type = type.toLowerCase().replace(/s$/, '')}s?\\/(\\d+)\\b`, 'i');
return globalXHR(`https://www.discogs.com/${type}/${id.toString()}`, { method: 'HEAD' }).then(function(response) {
let newId = rx.exec(response.finalUrl);
if (newId == null || !(newId = parseInt(newId[1])) || newId == id) return Promise.reject(reason);
return callback(type, newId);
}, reason2 => Promise.reject(reason));
}
function getDiscogsEntry(type, id) {
if (!type || !((type = type.toLowerCase() + 's') in dcEntriesCache)) throw 'Invalid item type';
if (!(id > 0)) return Promise.reject('Invalid item id');
if (dcEntriesCache[type].has(id)) return dcEntriesCache[type].get(id);
const result = queryDiscogsAPI(type + '/' + id.toString())
.catch(reason => discogsRedirectHandler(reason, type, id, getDiscogsEntry));
dcEntriesCache[type].set(id, result);
return result;
}
function getDiscogsArtistReleases(artistId) {
if (!(artistId > 0)) return Promise.reject('Invalid artist id');
if (dcArtistReleasesCache.has(artistId)) return dcArtistReleasesCache.get(artistId);
const getPage = (page = 1) => queryDiscogsAPI(`artists/${artistId}/releases`, { page: page, per_page: 500 });
const worker = getPage().then(function(response) {
const releases = response.releases;
if (!(response.pagination.page < response.pagination.pages)) return releases;
const fetchers = [ ];
for (let page = response.pagination.page; page < response.pagination.pages; ++page)
fetchers.push(getPage(page + 1));
return Promise.all(fetchers).then(responses =>
Array.prototype.concat.apply(releases, responses.map(response => response.releases)));
}, reason => discogsRedirectHandler(reason, 'srtist', artistId, (_, newId) => getDiscogsArtistReleases(newId))).catch(function(reason) {
console.warn(`Failed to get Discogs releases of ${artistId}:`, reason);
return [ ];
});
dcArtistReleasesCache.set(artistId, worker);
return worker;
}
function getDiscogsMatches(artistId, torrentGroups, resolveMasterYears = true) {
if (!(artistId > 0) || !Array.isArray(torrentGroups)) return Promise.reject('Invalid argument');
if (torrentGroups.length <= 0) return Promise.resolve([ ]);
return getDiscogsArtistReleases(artistId).then(function(releases) {
const masterLookups = new Set, results = torrentGroups.filter(function(torrentGroup) {
const titleNorm = [titleCmpNorm(torrentGroup.groupName), stripRlsSuffix(torrentGroup.groupName).toLowerCase()];
return releases.some(function(release) {
let strictMatch;
if (release.year < torrentGroup.groupYear || !(strictMatch = titleCmpNorm(release.title) == titleNorm[0])
&& jaroWrinkerSimilarity(stripRlsSuffix(release.title).toLowerCase(), titleNorm[1]) < sameTitleConfidence)
return false;
if (release.year == torrentGroup.groupYear) return true;
if (release.type == 'master') {
if (dcMasterYears.has(release.id)) return torrentGroup.groupYear == dcMasterYears.get(release.id);
if (resolveMasterYears) masterLookups.add(release.id);
else if (!['Main'].includes(release.role) && strictMatch) return true;
}
return false;
});
});
if (masterLookups.size > 0) console.log(masterLookups.size, 'master release(s) to lookup on Discogs');
return masterLookups.size <= 0 ? results : Promise.all(Array.from(masterLookups.values()).map(masterId =>
getDiscogsEntry('master', masterId).then(function(master) {
dcMasterYears.set(masterId, master.year);
return { [masterId]: master.year };
}))).then(results => Object.assign.apply({ }, results.filter(Boolean))).then(function(masterYears) {
//GM_setValue('discogs_master_years', Array.from(dcMasterYears).slice(-1000));
return results.concat(torrentGroups.filter(function(torrentGroup) {
const titleNorm = [titleCmpNorm(torrentGroup.groupName), stripRlsSuffix(torrentGroup.groupName).toLowerCase()];
return releases.some(release => release.year > torrentGroup.groupYear && release.type == 'master'
&& masterYears[release.id] == torrentGroup.groupYear && (titleCmpNorm(release.title) == titleNorm[0]
|| jaroWrinkerSimilarity(stripRlsSuffix(release.title).toLowerCase(), titleNorm[1]) >= sameTitleConfidence));
}));
});
}).then(torrentGroups => torrentGroups.map(torrentGroup => torrentGroup.groupId));
}
const discogsSearchArtist = (searchTerm = artist.name, anvs) => searchTerm ? queryDiscogsAPI('database/search', {
query: searchTerm,
type: 'artist',
sort: 'score',
sort_order: 'desc',
strict: !Array.isArray(anvs),
per_page: dcSearchSize || '',
}).then(function(response) {
const results = response.results.filter(result => result.type == 'artist' && (function() {
const anvMatch = anv => dcNameNormalizer(result.title).toLowerCase() == anv.toLowerCase()
|| jaroWrinkerSimilarity(dcNameNormalizer(result.title).toLowerCase(), anv.toLowerCase()) >= sameArtistConfidence;
return anvMatch(searchTerm) || Array.isArray(anvs) && anvs.map(dcNameNormalizer).some(anvMatch);
})());
if (results.length <= 0) {
const m = /^(.+?)\s*\((.+)\)$/.exec(searchTerm);
return m != null ? discogsSearchArtist(m[1], anvs).catch(function(reason) {
if (reason == 'No matches' && isNaN(parseInt(m[2]))) return discogsSearchArtist(m[2], anvs);
return Promise.reject(reason);
}) : Promise.reject('No matches');
}
console.log('[AAM] Discogs search results for "' + searchTerm + '":', results);
return results;
}) : Promise.reject('Invalid argument');
// MusicBrainz querying
const mbRequestsCache = new Map, mbArtistCache = new Map, mbArtistReleasesCache = new Map;
let mbLastRequest = null, mbLastCachedSuccess, mbQueriesCache;
function mbQueryAPI(endPoint, params) {
if (!endPoint) throw 'Endpoint is missing';
const url = new URL('http://musicbrainz.org/ws/2/' + endPoint.replace(/^\/+|\/+$/g, ''));
url.search = new URLSearchParams(Object.assign({ fmt: 'json' }, params));
const cacheKey = url.pathname.slice(6) + url.search;
if (mbRequestsCache.has(cacheKey)) return mbRequestsCache.get(cacheKey);
if (!mbQueriesCache && 'mbQueriesCache' in sessionStorage
&& (parseInt(sessionStorage.aamCachedArtistId) == artist.id
|| sessionStorage.mbQueriesCache.length < cacheSizeReserve * 2**10)) try {
mbQueriesCache = new Map(JSON.parse(sessionStorage.getItem('mbQueriesCache')));
} catch(e) { console.warn(e) }
if (!mbQueriesCache) mbQueriesCache = new Map;
if (mbQueriesCache.has(cacheKey)) return Promise.resolve(mbQueriesCache.get(cacheKey));
const worker = new Promise((resolve, reject) => { (function request() {
if (mbLastRequest == Infinity) return setTimeout(request, 100);
const now = Date.now();
if (now <= mbLastRequest + 1000) return setTimeout(request, mbLastRequest + 1000 - now);
mbLastRequest = Infinity;
globalXHR(url, { responseType: 'json' }).then(function({response}) {
mbLastRequest = Date.now();
mbQueriesCache.set(cacheKey, response);
if (!domStorageLimitReached) {
const serialized = JSON.stringify(Array.from(mbQueriesCache));
try {
sessionStorage.setItem('mbQueriesCache', serialized);
mbLastCachedSuccess = serialized;
} catch(e) {
console.warn(e, `(${serialized.length})`);
if (/\b(?:NS_ERROR_DOM_QUOTA_REACHED)\b/.test(e)
|| e instanceof DOMException && e.name == 'QuotaExceededError') {
domStorageLimitReached = true;
sessionStorage.setItem('mbQueriesCache', mbLastCachedSuccess);
mbLastCachedSuccess = undefined;
}
}
sessionStorage.setItem('aamCachedArtistId', artist.id);
}
resolve(response);
}, function(reason) {
mbLastRequest = Date.now();
if (/^(?:HTTP error 503)\b/.test(reason)) request(); else reject(reason);
});
})() });
mbRequestsCache.set(cacheKey, worker);
return worker;
}
function mbGetArtist(artistId) {
if (!artistId) return Promise.reject('Invalid artist id');
if (mbArtistCache.has(artistId)) return mbArtistCache.get(artistId);
const worker = mbQueryAPI('artist/' + artistId, { inc: ['url-rels', 'artist-rels', 'release-group-rels'].join('+') });
mbArtistCache.set(artistId, worker);
return worker;
}
function mbGetArtistReleases(artistId) {
if (!artistId) return Promise.reject('Invalid artist id');
if (mbArtistReleasesCache.has(artistId)) return mbArtistReleasesCache.get(artistId);
const worker = Promise.all([(function loadPage(offset = 0) {
return mbQueryAPI('release-group', { artist: artistId, offset: offset, limit: 9999 }).then(function(response) {
if (!Array.isArray(response['release-groups'])) return [ ];
return (offset += response['release-groups'].length) < response['release-group-count'] ?
loadPage(offset).then(releaseGroups => response['release-groups'].concat(releaseGroups))
: response['release-groups'];
});
})(),/* (function loadPage(offset = 0) {
return mbQueryAPI('recording', { artist: artistId, offset: offset, limit: 9999 }).then(function(response) {
if (!Array.isArray(response.recordings)) return [ ];
return (offset += response.recordings.length) < response['recording-count'] ?
loadPage(offset).then(recordings => response.recordings.concat(recordings)) : response.recordings;
});
})()*/]).then(results => Array.prototype.concat.apply([ ], results));
mbArtistReleasesCache.set(artistId, worker);
return worker;
}
const mbGetArtistMatches = (artistId, torrentGroups) => artistId && Array.isArray(torrentGroups) ?
torrentGroups.length > 0 ? mbGetArtistReleases(artistId).then(releaseGroups => torrentGroups.filter(function(torrentGroup) {
const titleNorm = [titleCmpNorm(torrentGroup.groupName), stripRlsSuffix(torrentGroup.groupName).toLowerCase()];
return releaseGroups.some(function(releaseGroup) {
const firstReleaseDate = new Date(releaseGroup['first-release-date']);
let strictMatch;
if (!(firstReleaseDate.getFullYear() >= torrentGroup.groupYear)
|| !(strictMatch = titleCmpNorm(releaseGroup.title) == titleNorm[0])
&& jaroWrinkerSimilarity(stripRlsSuffix(releaseGroup.title).toLowerCase(), titleNorm[1]) < sameTitleConfidence) return false;
if (firstReleaseDate.getFullYear() == torrentGroup.groupYear) return true;
return false;
});
})).then(torrentGroups => torrentGroups.map(torrentGroup => torrentGroup.groupId)) : Promise.resolve([ ]) : Promise.reject('Invalid argument');
const mbSearchArtist = (searchTerm = artist.name, anvs) => searchTerm ? mbQueryAPI('artist', {
query: '"' + searchTerm + '"',
limit: 100,
}).then(function(response) {
if (!Array.isArray(response.artists) || response.artists.length <= 0) return Promise.reject('No matches');
const results = response.artists.filter(function(artist) {
function anvMatch(anv) {
anv = anv.toLowerCase();
const propMatch = prop => prop && (prop = prop.toLowerCase())
&& (prop == anv || jaroWrinkerSimilarity(prop, anv) >= sameArtistConfidence);
const entityMatch = entity => ['name', 'sort-name'].some(propName => propMatch(entity[propName]));
return entityMatch(artist) || Array.isArray(artist.aliases) && artist.aliases.some(entityMatch);
}
return anvMatch(searchTerm) || Array.isArray(anvs) && anvs.some(anvMatch);
});
if (results.length <= 0) return Promise.reject('No matches');
console.log('[AAM] MusicBrainz search results for "' + searchTerm + '":', results);
return results;
}).catch(function(reason) {
if (reason == 'No matches') {
const m = /^(.+?)\s*\((.+)\)$/.exec(searchTerm);
if (m != null) return mbSearchArtist(m[1], anvs).catch(reason =>
reason == 'No matches' && isNaN(parseInt(m[2])) ? mbSearchArtist(m[2], anvs) : Promise.reject(reason));
}
return Promise.reject(reason);
}) : Promise.reject('Invalid argiment');
// BeatPort querying
const bpRequestsCache = new Map, bpArtistCache = new Map, bpArtistReleasesCache = new Map;
let bpLastCachedSuccess, bpQueriesCache, bsOAuth2Token = null;
function bpQueryAPI(endPoint, params) {
function getOauth2Token() {
function isTokenValid(accessToken) {
return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
&& accessToken.expires_at >= Date.now() + 10 * 1000;
}
if ('beatsourceAccessToken' in localStorage) try {
let accessToken = JSON.parse(localStorage.beatsourceAccessToken);
if (isTokenValid(accessToken)) {
console.info('Re-used Beatsource access token:', accessToken,
'expires at', new Date(accessToken.expires_at).toTimeString(),
'(' + ((accessToken.expires_at - Date.now()) / 1000) + ')');
return Promise.resolve(accessToken);
}
} catch(e) { }
const root = 'https://www.beatsource.com/';
const timeStamp = Date.now();
return globalXHR(root, { method: 'HEAD' }).then(function(response) {
let matches = /\b(?:btsrcSession)=([^\s\;]+)/m.exec(response.responseHeaders);
if (matches == null) return Promise.reject('cookie already set');
let result = JSON.parse(decodeURIComponent(matches[1]));
matches = /\b(?:sessionId)=([^\s\;]+)/m.exec(response.responseHeaders);
if (matches != null) try { result.sessionId = decodeURIComponent(matches[1]) } catch(e) { console.warn(e) }
return result;
}).catch(reason => globalXHR(root).then(function(response) {
let nextData = response.document.getElementById('__NEXT_DATA__');
if (nextData != null) nextData = JSON.parse(nextData.text); else return Promise.reject('object is missing');
return Object.assign(nextData.props.rootStore.authStore.user, {
apiHost: nextData.runtimeConfig.API_HOST,
clientId: nextData.runtimeConfig.API_CLIENT_ID,
recurlyPublicKey: nextData.runtimeConfig.RECURLY_PUBLIC_KEY,
});
})).then(function(accessToken) {
const tzOffset = new Date().getTimezoneOffset() * 60 * 1000;
if (!accessToken.timestamp) accessToken.timestamp = timeStamp;
if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp + accessToken.expires_in * 1000;
else accessToken.expires_at -= tzOffset;
if (!isTokenValid(accessToken)) {
console.warn('Received invalid Beatsource token:', accessToken);
return Promise.reject('invalid token received');
}
localStorage.beatsourceAccessToken = JSON.stringify(accessToken);
console.log('Beatsource access token successfully set:',
accessToken, (Date.now() - accessToken.timestamp) / 1000);
return accessToken;
});
}
if (!endPoint) throw 'Endpoint is missing';
const url = new URL('https://api.beatport.com/v4/' + endPoint.replace(/^\/+/g, ''));
url.search = new URLSearchParams(Object.assign({ }, params));
const cacheKey = url.pathname.slice(4) + url.search;
if (bpRequestsCache.has(cacheKey)) return bpRequestsCache.get(cacheKey);
if (!bpQueriesCache && 'bpQueriesCache' in sessionStorage
&& (parseInt(sessionStorage.aamCachedArtistId) == artist.id
|| sessionStorage.bpQueriesCache.length < cacheSizeReserve * 2**10)) try {
bpQueriesCache = new Map(JSON.parse(sessionStorage.getItem('bpQueriesCache')));
} catch(e) { console.warn(e) }
if (!bpQueriesCache) bpQueriesCache = new Map;
if (bpQueriesCache.has(cacheKey)) return Promise.resolve(bpQueriesCache.get(cacheKey));
const worker = (bsOAuth2Token || (bsOAuth2Token = getOauth2Token())).then(oauth2Token => globalXHR(url, {
responseType: 'json',
headers: { 'Authorization': oauth2Token.token_type + ' ' + oauth2Token.access_token },
}).then(function({response}) {
// bpQueriesCache.set(cacheKey, response);
// if (!domStorageLimitReached) {
// const serialized = JSON.stringify(Array.from(bpQueriesCache));
// try {
// sessionStorage.setItem('bpQueriesCache', serialized);
// bpLastCachedSuccess = serialized;
// } catch(e) {
// console.warn(e, `(${serialized.length})`);
// if (/\b(?:NS_ERROR_DOM_QUOTA_REACHED)\b/.test(e)
// || e instanceof DOMException && e.name == 'QuotaExceededError') {
// domStorageLimitReached = true;
// sessionStorage.setItem('bpQueriesCache', bpLastCachedSuccess);
// bpLastCachedSuccess = undefined;
// }
// }
// sessionStorage.setItem('aamCachedArtistId', artist.id);
// }
return response;
}));
bpRequestsCache.set(cacheKey, worker);
return worker;
}
function bpGetArtist(artistId) {
if (!(artistId > 0)) return Promise.reject('Invalid artist id');
if (bpArtistCache.has(artistId)) return bpArtistCache.get(artistId);
const worker = bpQueryAPI('catalog/artists/' + artistId + '/');
bpArtistCache.set(artistId, worker);
return worker;
}
const bpReflowArtistBio = bpArtist => bpArtist && bpArtist.bio
&& bpArtist.bio.replace(/(?:\r?\n)+/g, ' ').replace(/\s+/g, ' ');
function bpGetArtistReleases(artistId) {
if (!(artistId > 0)) return Promise.reject('Invalid artist id');
if (bpArtistReleasesCache.has(artistId)) return bpArtistReleasesCache.get(artistId);
const worker = bpQueryAPI('catalog/releases/', {
artist_id: artistId,
//page: page,
per_page: 9999,
}).then(response => response.results);
bpArtistReleasesCache.set(artistId, worker);
return worker;
}
const bpGetArtistMatches = (artistId, torrentGroups) => artistId > 0 && Array.isArray(torrentGroups) ?
torrentGroups.length > 0 ? bpGetArtistReleases(artistId).then(releases => torrentGroups.filter(function(torrentGroup) {
const titleNorm = [titleCmpNorm(torrentGroup.groupName), stripRlsSuffix(torrentGroup.groupName).toLowerCase()];
return releases.some(function(release) {
const newReleaseDate = new Date(release.new_release_date), publishDate = new Date(release.publish_date);
console.assert(!isNaN(newReleaseDate), 'Invalid release date in MusicBrainz data: ' + release.new_release_date);
console.assert(!isNaN(publishDate), 'Invalid publish date in MusicBrainz data: ' + release.publish_date);
const yearMatch = newReleaseDate.getFullYear() == torrentGroup.groupYear;
if (!yearMatch && !(newReleaseDate.getFullYear() >= torrentGroup.groupYear)) return false;
return titleCmpNorm(release.name) == titleNorm[0] || yearMatch
&& jaroWrinkerSimilarity(stripRlsSuffix(release.name).toLowerCase(), titleNorm[1]) >= sameTitleConfidence;
});
})).then(torrentGroups => torrentGroups.map(torrentGroup => torrentGroup.groupId)) : Promise.resolve([ ]) : Promise.reject('Invalid argument');
const bpSearchArtist = (searchTerm = artist.name, anvs) => searchTerm ? bpQueryAPI('catalog/search/', {
q: '"' + searchTerm + '"',
type: 'artists',
per_page: 100,
}).then(function(response) {
if (response.count <= 0) return Promise.reject('No matches');
const results = response.artists.filter(function(artist) {
if (!artist.name) return false;
const name = artist.name.toLowerCase();
function anvMatch(anv) {
if (!anv) return false; else if ((anv = anv.toLowerCase()) == name) return true;
const score = jaroWrinkerSimilarity(anv, name);
if (score < sameArtistConfidence) return false;
//console.log('[AAM] Jaro-Wrinker fuzzy match:', name, anv, `(${score.toFixed(3)})`)
return true;
}
return anvMatch(searchTerm) || Array.isArray(anvs) && anvs.some(anvMatch);
});
if (results.length <= 0) return Promise.reject('No matches');
console.log('[AAM] BeatPort search results for "' + searchTerm + '":', results);
return results;
}).catch(function(reason) {
if (reason == 'No matches') {
const m = /^(.+?)\s*\((.+)\)$/.exec(searchTerm);
if (m != null) return bpSearchArtist(m[1], anvs).catch(reason =>
reason == 'No matches' && isNaN(parseInt(m[2])) ? bpSearchArtist(m[2], anvs) : Promise.reject(reason));
}
return Promise.reject(reason);
}) : Promise.reject('Invalid argiment');
// Apple Music querying
const amRequestsCache = new Map, amArtistCache = new Map, amArtistReleasesCache = new Map;
let amLastCachedSuccess, amQueriesCache, amDesktopEnvironment = null;
const amQueryAPI = (endPoint, params) => endPoint ? (amDesktopEnvironment || (amDesktopEnvironment = (function() {
if ('appleMusicDesktopConfig' in sessionStorage) try {
return Promise.resolve(JSON.parse(sessionStorage.getItem('appleMusicDesktopConfig')));
} catch(e) { console.warn('Apple Music invalid cached desktop config:', e) }
return globalXHR('https://music.apple.com/').then(function({document}) {
let config = document.head.querySelector('meta[name="desktop-music-app/config/environment"][content]');
if (config != null) config = JSON.parse(decodeURIComponent(config.content));
else return Promise.reject('Apple desktop environment missing');
if (!config.MEDIA_API.token) {
console.warn('Apple Music received invalid desktop config:', config);
return Promise.reject('Apple API token missing')
}
console.log('Sucecssfully extracted Apple desktop environment:', config);
sessionStorage.setItem('appleMusicDesktopConfig', JSON.stringify(config));
return config;
});
})())).then(function(environment) {
const url = new URL(environment.MUSIC.BASE_URL + '/catalog/us/' + endPoint.replace(/^\/+/g, ''));
if (params) url.search = new URLSearchParams(params);
const cacheKey = url.pathname.slice(15) + url.search;
if (amRequestsCache.has(cacheKey)) return amRequestsCache.get(cacheKey);
if (!amQueriesCache && 'amQueriesCache' in sessionStorage
&& (parseInt(sessionStorage.aamCachedArtistId) == artist.id
|| sessionStorage.amQueriesCache.length < cacheSizeReserve * 2**10)) try {
amQueriesCache = new Map(JSON.parse(sessionStorage.getItem('amQueriesCache')));
} catch(e) { console.warn(e) }
if (!amQueriesCache) amQueriesCache = new Map;
if (amQueriesCache.has(cacheKey)) return Promise.resolve(amQueriesCache.get(cacheKey));
url.searchParams.set('omit[resource]', 'relationships,views,meta,autos');
url.searchParams.set('l', 'en-us');
url.searchParams.set('platform', 'web');
const worker = globalXHR(url, {
responseType: 'json',
headers: { 'Authorization': 'Bearer ' + environment.MEDIA_API.token },
}).then(function({response}) {
// amQueriesCache.set(cacheKey, response);
// if (!domStorageLimitReached) {
// const serialized = JSON.stringify(Array.from(amQueriesCache));
// try {
// sessionStorage.setItem('amQueriesCache', serialized);
// amLastCachedSuccess = serialized;
// } catch(e) {
// console.warn(e, `(${serialized.length})`);
// if (/\b(?:NS_ERROR_DOM_QUOTA_REACHED)\b/.test(e)
// || e instanceof DOMException && e.name == 'QuotaExceededError') {
// domStorageLimitReached = true;
// sessionStorage.setItem('amQueriesCache', amLastCachedSuccess);
// amLastCachedSuccess = undefined;
// }
// }
// sessionStorage.setItem('aamCachedArtistId', artist.id);
// }
return response;
});
amRequestsCache.set(cacheKey, worker);
return worker;
}) : Promise.reject('Endpoint is missing');
function amGetArtist(artistId) {
if (!(artistId > 0)) return Promise.reject('Invalid artist id');
if (amArtistCache.has(artistId)) return amArtistCache.get(artistId);
const worker = amQueryAPI('artists/' + artistId, { extend: 'artistBio,bornOrFormed,isGroup,origin' })
.then(response => response.data[0]);
amArtistCache.set(artistId, worker);
return worker;
}
function amGetArtistAlbums(artistId) {
if (!(artistId > 0)) return Promise.reject('Invalid artist id');
if (amArtistReleasesCache.has(artistId)) return amArtistReleasesCache.get(artistId);
const worker = (function getPage(offset = 0) {
return amQueryAPI(`artists/${artistId}/albums`, { offset: offset, limit: 100 }).then(response =>
!response.next ? response.data : getPage(offset + response.data.length).then(data => response.data.concat(data)));
})();
amArtistReleasesCache.set(artistId, worker);
return worker;
}
const amGetArtistMatches = (artistId, torrentGroups) => artistId > 0 && Array.isArray(torrentGroups) ?
torrentGroups.length > 0 ? amGetArtistAlbums(artistId).then(albums => torrentGroups.filter(function(torrentGroup) {
const titleNorm = [titleCmpNorm(torrentGroup.groupName), stripRlsSuffix(torrentGroup.groupName).toLowerCase()];
return albums.some(function(album) {
const releaseDate = new Date(album.attributes.releaseDate);
console.assert(!isNaN(releaseDate), 'Invalid release date in Apple Music data: ' + album.releaseDate);
const yearMatch = releaseDate.getFullYear() == torrentGroup.groupYear;
if (!yearMatch && !(releaseDate.getFullYear() >= torrentGroup.groupYear)) return false;
return titleCmpNorm(album.attributes.name) == titleNorm[0] || yearMatch
&& jaroWrinkerSimilarity(stripRlsSuffix(album.attributes.name).toLowerCase(), titleNorm[1]) >= sameTitleConfidence;
});
})).then(torrentGroups => torrentGroups.map(torrentGroup => torrentGroup.groupId)) : Promise.resolve([ ]) : Promise.reject('Invalid argument');
const amSearchArtist = (searchTerm = artist.name, anvs) => searchTerm ? amQueryAPI('search', {
term: '"' + searchTerm + '"',
types: 'artists',
}).then(function(response) {
if (!response.results || !response.results.artists || !response.results.artists.data
|| response.results.artists.data.length <= 0) return Promise.reject('No matches');
const results = response.results.artists.data.filter(function(artist) {
if (artist.type != 'artists' || !artist.attributes) return false;
const artistName = artist.attributes.name.toLowerCase();
function anvMatch(anv) {
if (!anv) return false; else if ((anv = anv.toLowerCase()) == artistName) return true;
const score = jaroWrinkerSimilarity(anv, artistName);
if (score < sameArtistConfidence) return false;
//console.log('[AAM] Jaro-Wrinker fuzzy match:', artistName, anv, `(${score.toFixed(3)})`)
return true;
}
return anvMatch(searchTerm) || Array.isArray(anvs) && anvs.some(anvMatch);
});
if (results.length <= 0) return Promise.reject('No matches');
console.log('[AAM] Apple Music search results for "' + searchTerm + '":', results);
return results;
}).catch(function(reason) {
if (reason == 'No matches') {
const m = /^(.+?)\s*\((.+)\)$/.exec(searchTerm);
if (m != null) return amSearchArtist(m[1], anvs).catch(reason =>
reason == 'No matches' && isNaN(parseInt(m[2])) ? amSearchArtist(m[2], anvs) : Promise.reject(reason));
}
return Promise.reject(reason);
}) : Promise.reject('Invalid argiment');
if (artistEdit) {
String.prototype.toASCII = function() {
return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
};
String.prototype.properTitleCase = function() {
return [this.toUpperCase(), this.toLowerCase()].some(str => this == str) ? this
: caseFixes.reduce((result, replacer) => result.replace(...replacer), this);
};
const caseFixes = [
[
new RegExp(`(\\w+|[\\,\\)\\]\\}\\"\\'\\‘\\’\\“\\‟\\”]) +(${[
'And In', /*'And His', 'And Her', */'And', 'By A', 'By An', 'By The', 'By', 'Feat.', 'Ft.', 'For A',
'For An', 'For', 'From', 'If', 'In To', 'In', 'Into', 'Nor', 'Not', 'Of An', 'Of The', 'Of',
'Off', 'On', 'Onto', 'Or', 'Out Of', 'Out', 'Over', 'With', 'Without', 'Yet',
'Y Su', 'Y Sua', 'Y Suo', 'De', 'Y', 'E La Sua', 'E Sua', 'E Il Suo', 'La Sua', 'E Le Sue', 'Le Sue', 'E Sue',
'Et Son', 'Et Ses', 'Et Le', 'Et Sa', 'Et Sua', 'E Seu', 'Di',
'Und Sein', 'Und Seine', 'Und', 'Mit Seinem', 'Mit Seiner', 'Mit',
'En Zijn', 'Og',
].join('|')})(?=\\s+)`, 'g'), (match, preWord, shortWord) => preWord + ' ' + shortWord.toLowerCase(),
], [
new RegExp(`\\b(${['by', 'in', 'of', 'on', 'or', 'to', 'for', 'out', 'into', 'from', 'with'].join('|')})$`, 'g'),
(match, shortWord) => ' ' + shortWord[0].toUpperCase() + shortWord.slice(1).toLowerCase(),
],
[/([\-\:\&\;]) +(the|an?)(?=\s+)/g, (match, sym, article) => sym + ' ' + article[0].toUpperCase() + article.slice(1).toLowerCase()],
];
function rmDelLink(li) {
for (let a of li.getElementsByTagName('A')) if (a.textContent.trim() == 'X') li.removeChild(a);
}
let aliasesRoot = document.body.querySelector('form.add_form');
if (aliasesRoot != null) (aliasesRoot = aliasesRoot.parentNode.parentNode).id = 'aliases';
else throw 'Add alias form could not be located';
console.assert(aliasesRoot.nodeName == 'DIV' && aliasesRoot.className == 'box pad',
"aliasesRoot.nodeName == 'DIV' && aliasesRoot.className == 'box pad'");
const aliases = aliasesRoot.querySelectorAll('div.box > div > ul > li'),
dropDown = aliasesRoot.querySelector('select[name="redirect"]');
const epitaph = `
Don't navigate away, close or reload current page, till it reloads self.
The operation can take longer to complete and can be reverted only
by hand, sure to proceed?`;
let mainIdentityId, inProgress = false;
// Combined search with cross identifications... time expensive!
const useMusicBrainz = GM_getValue('use_musicbrainz', true);
const useAppleMusic = GM_getValue('use_applemusic', true);
const useBeatPort = GM_getValue('use_beatport', true);
function addSearchResultsTotals(results) {
if (!Array.isArray(results)) throw 'Invalid argument';
if (results.length <= 0) return results;
const hasMG = result => result && 'matchedGroups' in result && Array.isArray(result.matchedGroups);
const maxMatches = Math.max(...results.map(result => hasMG(result) ? result.matchedGroups.length : -1));
const maxIndex = maxMatches > 0 ? results.findIndex(result =>
hasMG(result) && result.matchedGroups.length == maxMatches) : 0;
return Object.assign(results, { bestMatch: results[maxIndex] });
}
function stripDcRelativesFromResults(results, pivot) {
if (!results) throw 'Invalid argument';
return pivot && results.length > 1 ? addSearchResultsTotals(results.filter(function(result) {
if (!result.discogsArtist) return true;
return !['aliases', 'groups', 'members'].some(propName => Array.isArray(pivot[propName])
&& pivot[propName].some(linkedArtist => linkedArtist.id == result.discogsArtist.id));
})) : results;
}
function searchArtist(searchTerm = artist.name, consolidateDcRelatives = false, torrentGroups, anvs) {
if (!searchTerm) return Promise.reject('Invalid argiment');
const dcLookup = discogsSearchArtist(searchTerm, anvs).then(results => results.map(function(result) {
result = Object.assign({ }, result, { uri: 'https://www.discogs.com' + result.uri });
for (let key of ['user_data']) if (key in result) delete result[key];
return { discogsArtist: result };
}));
const mbLookup = useMusicBrainz ? mbSearchArtist(searchTerm, anvs).then(results => Promise.all(results.map(result =>
mbGetArtist(result.id).catch(reason => result).then(function(artist, ndx) {
const result = { mbArtist: Object.defineProperty(Object.assign({ }, artist), 'uri', {
get: function() { return 'https://musicbrainz.org/artist/' + this.id },
}) };
const discogsIds = artist.relations.map(function(relation) {
if (relation['target-type'] != 'url') return false;
if (!relation.type || relation.type.toLowerCase() != 'discogs') return false;
const discogsId = /\/artist\/(\d+)\b/i.exec(relation.url.resource);
return discogsId != null && parseInt(discogsId[1]);
}).filter(Boolean);
if (discogsIds.length > 1) console.warn('[AAM] MusicBrainz artist profile bound to more Discogs profiles, keeping the first:',
artist, discogsIds.map(discogsId => 'https://www.discogs.com/artist/' + discogsId));
if (discogsIds.length > 0) result.discogsArtist = {
id: discogsIds[0],
title: artist.name,
uri: 'https://www.discogs.com/artist/' + discogsIds[0],
};
return result;
})))) : Promise.reject('Not used');
const amLookup = useAppleMusic ? amSearchArtist(searchTerm, anvs).then(results => Promise.all(results.map(result =>
amGetArtist(parseInt(result.id)).catch(reason => result).then(artist => ({
amArtist: Object.defineProperty(Object.assign({ }, artist), 'uri', {
get: function() { return this.attributes.url.replace(/\?.*$/, '') },
}),
}))))) : Promise.reject('Not used');
const bpLookup = useBeatPort ? bpSearchArtist(searchTerm, anvs).then(results => Promise.all(results.map(result =>
bpGetArtist(result.id).catch(reason => result).then(artist => ({
bpArtist: Object.defineProperty(Object.assign({ }, artist), 'uri', {
get: function() { return 'https://www.beatport.com/artist/' + this.slug + '/' + this.id/* + '/releases'*/ },
}),
}))))) : Promise.reject('Not used');
return Promise.all([dcLookup.catch(function(reason) {
console.log('[AAM] Discogs:', reason);
return Promise.resolve(null);
}), mbLookup.catch(function(reason) {
console.log('[AAM] MusicBrainz:', reason);
return Promise.resolve(null);
}), amLookup.catch(function(reason) {
console.log('[AAM] Apple Music:', reason);
return Promise.resolve(null);
}), bpLookup.catch(function(reason) {
console.log('[AAM] BeatPort:', reason);
return Promise.resolve(null);
})]).then(function(results) {
function mergeResults(target, source) {
for (let key in source) if (!(key in target)) target[key] = source[key];
}
const combinedResults = results[0] || [ ];
// MusicBrainz
if (results[1]) for (let mbResult of results[1]) {
const linkedResult = mbResult.discogsArtist && combinedResults.find(result =>
result.discogsArtist && result.discogsArtist.id == mbResult.discogsArtist.id);
if (linkedResult) mergeResults(linkedResult, mbResult); else combinedResults.push(mbResult);
}
for (let index of [2, 3]) Array.prototype.push.apply(combinedResults, results[index]);
return combinedResults.length > 0 ? combinedResults : Promise.reject('No matches');
}).then(function(results) {
function consolidateResults(preserveIndex, ditchIndex, mergeGroups = true) {
if (!(preserveIndex >= 0 && preserveIndex < results.length)
|| !(ditchIndex >= 0 && ditchIndex < results.length)) throw 'Index out of range';
if (!Array.isArray(results[preserveIndex].matchedGroups) || Array.isArray(results[ditchIndex].matchedGroups)
&& results[ditchIndex].matchedGroups.length > results[preserveIndex].matchedGroups.length)
[preserveIndex, ditchIndex] = [ditchIndex, preserveIndex]
console.log('[AAM] Consolidating search results:', results[ditchIndex], `[${ditchIndex}]`, '=>',
results[preserveIndex], `[${preserveIndex}]`);
if (mergeGroups && [preserveIndex, ditchIndex].every(index => Array.isArray(results[index].matchedGroups)))
Array.prototype.push.apply(results[preserveIndex].matchedGroups,
results[ditchIndex].matchedGroups.filter(groupId => !results[preserveIndex].matchedGroups.includes(groupId)));
for (let key in results[ditchIndex]) if (!(key in results[preserveIndex]))
results[preserveIndex][key] = results[ditchIndex][key];
results.splice(ditchIndex, 1);
}
function consolidateResults3(scoreFunc) {
if (typeof scoreFunc != 'function') throw 'The parameter must be a valid callback';
do {
const scores = results.map((result1, ndx1) => results.map(function(result2, ndx2) {
if (ndx2 == ndx1) return -Infinity;
if (Object.keys(result2).some(siteKey => siteKey != 'matchedGroups'
&& Object.keys(result1).includes(siteKey))) return -1;
return scoreFunc(ndx1, ndx2) || 0;
}));
const scores2 = scores.map(scores => Math.max(...scores)), hiScore = Math.max(...scores2);
if (!(hiScore > 0)) break;
const ndx1 = scores2.indexOf(hiScore), ndx2 = scores[ndx1].indexOf(hiScore);
console.assert(ndx1 >= 0, 'ndx1 >= 0'); console.assert(ndx2 >= 0, 'ndx2 >= 0');
console.assert(ndx2 != ndx1, 'ndx2 != ndx1');
console.log(`[AAM] Matching results by highest score (${hiScore}):`,
results[ndx1], `[${ndx1}]`, results[ndx2], `[${ndx2}]`);
consolidateResults(ndx1, ndx2);
} while (true);
}
return (!isGenericArtist(searchTerm) && Array.isArray(torrentGroups) && torrentGroups.length > 0 ? Promise.all([
// Discogs
Promise.all(results.map(result => result.discogsArtist ?
getDiscogsMatches(result.discogsArtist.id, torrentGroups).catch(reason => null) : null)),
// MusicBrainz
Promise.all(results.map(result => result.mbArtist ?
mbGetArtistMatches(result.mbArtist.id, torrentGroups).catch(reason => null) : null)),
// Apple Music
Promise.all(results.map(result => result.amArtist ?
amGetArtistMatches(parseInt(result.amArtist.id), torrentGroups).catch(reason => null) : null)),
// BeatPort
Promise.all(results.map(result => result.bpArtist ?
bpGetArtistMatches(result.bpArtist.id, torrentGroups).catch(reason => null) : null)),
]).then(function(matchedGroups) {
if ((results = results.map((result, ndx) => Object.assign(result, { matchedGroups: (function() {
const result = new Set;
for (let mg of matchedGroups) if (Array.isArray(mg[ndx])) for (let groupId of mg[ndx]) result.add(groupId);
return Array.from(result);
})() }))).length < 2) return results;
consolidateResults3(function(ndx1, ndx2) {
if ([ndx1, ndx2].some(ndx => !Array.isArray(results[ndx].matchedGroups))) return 0;
const commonGroups = results[ndx2].matchedGroups.filter(groupId =>
results[ndx1].matchedGroups.includes(groupId));
if (commonGroups.length > 0) console.log('[AAM] Matching results by matching same torrent groups:',
results[ndx1], `[${ndx1}]`, results[ndx2], `[${ndx2}]`, commonGroups);
return commonGroups.length;
});
return results.length > 1 ? Promise.all(results.map(result => [
/* 0 */ result.discogsArtist ? getDiscogsArtistReleases(result.discogsArtist.id) : Promise.resolve(null),
/* 1 */ result.mbArtist ? mbGetArtistReleases(result.mbArtist.id) : Promise.resolve(null),
/* 2 */ result.amArtist ? amGetArtistAlbums(result.amArtist.id) : Promise.resolve(null),
/* 3 */ result.bpArtist ? bpGetArtistReleases(result.bpArtist.id) : Promise.resolve(null),
].map(promise => promise.catch(reason => null))).map(Promise.all.bind(Promise))).then(function(releaseLists) {
let dcMasterRequests = new Set;
function getNormalizedResult(release, index) {
switch (index) {
case 0: return [ // Discogs
release.title ? titleCmpNorm(release.title) : null,
release.title ? stripRlsSuffix(release.title).toLowerCase() : null,
release.type == 'master' && dcMasterYears.get(release.id) || release.year,
release.type != 'master' || dcMasterYears.has(release.id) ? 0 : 2,
];
case 1: return [ // MusicBrainz
release.title ? titleCmpNorm(release.title) : null,
release.title ? stripRlsSuffix(release.title).toLowerCase() : null,
new Date(release['first-release-date']).getFullYear(),
0,
];
case 2: return [ // Apple Music
release.attributes.name ? titleCmpNorm(release.attributes.name) : null,
release.attributes.name ? stripRlsSuffix(release.attributes.name).toLowerCase() : null,
new Date(release.attributes.releaseDate).getFullYear(),
1,
];
case 3: { // BeatPort
const newReleaseDate = new Date(release.new_release_date),
publishDate = new Date(release.publish_date);
return [
release.name ? titleCmpNorm(release.name) : null,
release.name ? stripRlsSuffix(release.name).toLowerCase() : null,
newReleaseDate.getFullYear(),
1,
];
}
default: throw 'Assertion failed: index out of range';
}
}
function matchesByReleaseList(ndx1, ndx2) {
if (releaseLists[ndx1].some((releaseList1, ndx) => releaseList1 && releaseLists[ndx2][ndx]))
return -1;
return Math.max(...releaseLists[ndx1].map(function(releaseList1, ndx12) {
if (releaseLists[ndx2][ndx12]) return -1; // avoid consolidation between distinct artists of the same source?
return releaseList1 ? releaseLists[ndx2].map(function(releaseList2, ndx22) {
return releaseList2 ? releaseList1.map(function(release1) {
const matchedCount = releaseList2.filter(function(release2) {
const normResults = [
getNormalizedResult(release1, ndx12),
getNormalizedResult(release2, ndx22),
];
if (normResults[0][3] == 0 && !(normResults[0][2] <= normResults[1][2])
|| normResults[1][3] == 0 && !(normResults[1][2] <= normResults[0][2])) return false;
const exactMatch = normResults[0][0] && normResults[1][0] && normResults[0][0] == normResults[1][0];
if (!exactMatch && jaroWrinkerSimilarity(normResults[0][1], normResults[1][1]) < sameTitleConfidence)
return false;
if (normResults[0][3] == 2 && (normResults[1][3] == 2 || normResults[0][2] >= normResults[1][2]))
dcMasterRequests.add(release1.id);
if (normResults[1][3] == 2 && (normResults[0][3] == 2 || normResults[1][2] >= normResults[0][2]))
dcMasterRequests.add(release2.id);
if ((normResults[0][3] != 0 || normResults[1][3] != 0 || normResults[0][2] != normResults[1][2])
&& (normResults[0][3] != 1 || normResults[1][3] == 1 || !(normResults[0][2] >= normResults[1][2]))
&& (normResults[1][3] != 1 || normResults[0][3] == 1 || !(normResults[1][2] >= normResults[0][2])))
return false;
if (!exactMatch && (normResults[0][3] > 0 && normResults[0][1] != stripRlsSuffix(searchTerm).toLowerCase()
|| normResults[1][3] > 0 && normResults[1][1] != stripRlsSuffix(searchTerm).toLowerCase())) return false;
console.log('[AAM] Matching releases:', release1, release2)
return true;
}).length;
const listSize = Math.min(releaseList1.length, releaseList2.length),
matchRatio = matchedCount / listSize;
if (matchedCount > 0) console.log('[AAM] Matching results by having common releases:',
results[ndx1], `[${ndx1}:${ndx12}]`, results[ndx2], `[${ndx2}:${ndx22}]`, `(${matchedCount}/${listSize})`);
return matchedCount;
}) : 0;
}) : 0;
}));
}
consolidateResults3(matchesByReleaseList);
if (results.length < 2 || dcMasterRequests.size <= 0) return results;
console.log(dcMasterRequests.size, 'master release(s) to lookup on Discogs');
dcMasterRequests = Array.from(dcMasterRequests).map(masterId => getDiscogsEntry('master', masterId)
.then(master => { dcMasterYears.set(masterId, master.year) }), console.warn.bind(console));
return Promise.all(dcMasterRequests).then(() => (consolidateResults3(matchesByReleaseList), results));
}) : results;
}) : Promise.resolve(results)).then(function(results) {
if (results.length < 2 || !consolidateDcRelatives) return results;
const dcArtistIds = results.map(result => result.discogsArtist && result.discogsArtist.id).filter(Boolean);
return dcArtistIds.length > 1 ? Promise.all(dcArtistIds.map(dcArtistid =>
getDiscogsEntry('artist', dcArtistid).then(discogsArtist =>
({ [dcArtistid]: discogsArtist }), reason => null))).then(results =>
(results = results.filter(Boolean)).length > 0 ? Object.assign.apply({ }, results) : null).then(function(discogsArtists) {
if (discogsArtists) (function iterate(offset = 0) { for (let index = offset; index < results.length; ++index) {
if (!results[index].discogsArtist) continue;
let relatives = discogsArtists[results[index].discogsArtist.id];
if (!relatives) continue; // assertion failed
relatives = Array.prototype.concat.apply([ ], ['aliases', 'groups'/*, 'members'*/].map(propName =>
propName in relatives ? relatives[propName].map(alias => alias.id) : null).filter(Boolean));
if (relatives.length <= 0) continue;
const relativeNdx = results.findIndex((result, relativeNdx) =>
relativeNdx != index && result.discogsArtist && relatives.includes(result.discogsArtist.id));
if (relativeNdx < 0) continue;
console.log('[AAM] Consolidating relative results:', results[index], results[relativeNdx]);
consolidateResults(index, relativeNdx);
return iterate(Math.min(index, relativeNdx));
} })();
return results;
}) : results;
}).then(function(results) {
const cacheSizes = ['discogsQueriesCache', 'mbQueriesCache', 'bpQueriesCache', 'amQueriesCache']
.map(key => key in sessionStorage ? sessionStorage[key].length : 0);
console.log(`[AAM] Combined search for '${searchTerm}' completed, final cache sizes:`,
cacheSizes.map(size => `${(size / 2**20).toFixed(2)}MiB`).join(' / '),
`(${(cacheSizes.reduce((acc, size) => acc + size, 0) / 2**20).toFixed(2)}MiB), DOM quota exceeded? ${domStorageLimitReached}`);
return results;
});
}).then(addSearchResultsTotals);
}
function getMatchedArtists(results) {
results = results.filter(result => Array.isArray(result.matchedGroups) && result.matchedGroups.length > 0);
if (results.length > 1) {
function matchName(result, name) {
name = name.toLowerCase();
if (result.discogsArtist && dcNameNormalizer(result.discogsArtist.title).toLowerCase() == name) return true;
if (result.mbArtist && result.mbArtist.name.toLowerCase() == name) return true;
if (result.amArtist && result.amArtist.attributes.name.toLowerCase() == name) return true;
if (result.bpArtist && result.bpArtist.name.toLowerCase() == name) return true;
return false;
}
const nameMatches = name => results.some(result => matchName(result, name));
const stripNameFromResults = name => { results = results.filter(result => !matchName(result, name)) };
for (let alias of aliases) {
if (!(alias = getAlias(alias)) || alias.name == artist.name) continue;
//stripNameFromResults(alias.name);
}
}
return addSearchResultsTotals(results);
}
decodeArtistTitles(artist);
for (let torrentGroup of artist.torrentgroup) {
if (torrentGroup.extendedArtists && Array.isArray(torrentGroup.extendedArtists[1])
&& torrentGroup.extendedArtists[1].length > 0 || artistlessGroups.has(torrentGroup.groupId)) continue;
let container = document.getElementById('artistless-groups');
if (container == null) {
const ref = aliasesRoot.querySelector(':scope > br:first-of-type'), hdr = document.createElement('H4');
hdr.innerHTML = 'List of artistless groups
(Bold printed groups are missing artist info entirely and are potential source for complex operations to fail; review and fix these groups first to avoid later problems)';
hdr.style = 'color: red; font-weight: bold;';
aliasesRoot.insertBefore(hdr, ref);
container = document.createElement('DIV');
container.id = 'artistless-groups';
container.style = 'padding: 1em;';
aliasesRoot.insertBefore(container, ref);
}
if (container.childElementCount > 0) container.append(', ');
const a = document.createElement('A');
a.href = '/torrents.php?id=' + torrentGroup.groupId;
a.target = '_blank';
a.textContent = torrentGroup.groupName || torrentGroup.groupId.toString();
a.style.fontWeight = !torrentGroup.extendedArtists ? 'bold' : 'normal';
container.append(a);
artistlessGroups.add(torrentGroup.groupId);
}
class TorrentGroupsManager {
constructor(aliasId) {
if (!(aliasId > 0)) throw 'Invalid argument';
this.groups = { };
for (let torrentGroup of artist.torrentgroup) {
console.assert(!(torrentGroup.groupId in this.groups), '!(torrentGroup.groupId in this.groups)');
if (!torrentGroup.extendedArtists) continue;
const importances = Object.keys(torrentGroup.extendedArtists)
.filter(importance => Array.isArray(torrentGroup.extendedArtists[importance])
&& torrentGroup.extendedArtists[importance].some(artist => artist.aliasid == aliasId))
.map(key => parseInt(key)).filter((el, ndx, arr) => arr.indexOf(el) == ndx);
if (importances.length > 0) this.groups[torrentGroup.groupId] = importances;
}
}
get size() {
return this.groups ? Object.keys(this.groups).filter(groupId =>
Array.isArray(this.groups[groupId]) && this.groups[groupId].length > 0).length : 0;
}
get aliasUsed() { return this.size > 0 }
removeAliasFromGroups() {
if (!this.aliasUsed) return Promise.resolve('No groups block this alias');
const groupIds = Object.keys(this.groups), removeAliasFromGroup = [
groupId => deleteArtistFromGroup(groupId, artist.id, this.groups[groupId]),
function(index = 0) {
if (!(index >= 0 && index < groupIds.length))
return Promise.resolve('Artist alias removed from all groups');
const importances = this.groups[groupIds[index]];
console.assert(Array.isArray(importances) && importances.length > 0,
'Array.isArray(importances) && importances.length > 0');
return Array.isArray(importances) && importances.length > 0 ?
deleteArtistFromGroup(groupIds[index], artist.id, importances)
.then(results => removeAliasFromGroup[1].call(this, index + 1))
: removeAliasFromGroup[1].call(this, index + 1);
},
];
return (groupIds.length > 100 ? removeAliasFromGroup[1].call(this) : groupIds.length > 1 ?
Promise.all(groupIds.slice(0, -1).map(removeAliasFromGroup[0])).then(() =>
wait(groupIds[groupIds.length - 1]).then(removeAliasFromGroup[0])).catch(function(reason) {
console.warn('TorrentGroupsManager.removeAliasFromGroups parallely failed, trying serially:', reason);
return removeAliasFromGroup[1].call(this);
}) : removeAliasFromGroup[0](groupIds[groupIds.length - 1])).then(wait);
}
addAliasToGroups(aliasName = artist.name) {
if (!this.aliasUsed) return Promise.resolve('No groups block this alias');
if (!aliasName) return Promise.reject('Argument is invalid');
const groupIds = Object.keys(this.groups), _addAliasToGroup = [
groupId => addAliasToGroup(groupId, aliasName, this.groups[groupId]),
function(index = 0) {
if (!(index >= 0 && index < groupIds.length)) return Promise.resolve('Artist alias re-added to all groups');
const importances = this.groups[groupIds[index]];
console.assert(Array.isArray(importances) && importances.length > 0,
'Array.isArray(importances) && importances.length > 0');
return Array.isArray(importances) && importances.length > 0 ?
addAliasToGroup(groupIds[index], aliasName, importances)
.then(result => _addAliasToGroup[1].call(this, index + 1))
: _addAliasToGroup[1].call(this, index + 1);
}
];
return groupIds.length > 100 ? _addAliasToGroup[1].call(this).then(wait) : groupIds.length > 1 ?
_addAliasToGroup[0](groupIds[0]).then(wait).then(() =>
Promise.all(groupIds.slice(1).map(_addAliasToGroup[0]))).catch(function(reason) {
console.warn('TorrentGroupsManager.addAliasToGroups parallely failed, trying serially:', reason);
return _addAliasToGroup[1].call(this).then(wait);
}) : _addAliasToGroup[0](groupIds[0]).then(wait);
}
}
class AliasDependantsManager {
constructor(aliasId) {
console.assert(aliasId > 0, 'aliasId > 0');
if (aliasId > 0) this.redirectTo = aliasId; else throw 'Invalid argument';
if ((this.aliases = Array.from(aliases).map(function(li) {
const alias = getAlias(li);
if (alias && alias.redirectId == aliasId) return alias;
}).filter(Boolean)).length <= 0) delete this.aliases;
}
get size() { return Array.isArray(this.aliases) ? this.aliases.length : 0 }
get hasDependants() { return this.size > 0 }
removeAll() {
return this.hasDependants ? Promise.all(this.aliases.map(function(alias) {
let worker = Promise.resolve();
if (alias.tgm.aliasUsed) worker = alias.tgm.removeAliasFromGroups();
return worker.then(() => deleteAlias(alias.id));
})) : Promise.resolve('No dependants');
}
restoreAll(redirectTo = this.redirectTo, artistIdOrName) {
return this.hasDependants ? resolveArtistId(artistIdOrName)
.then(artistId => resolveAliasId(redirectTo, (artistIdOrName || !(redirectTo >= 0)) && artistId, true)
.then(redirectTo => Promise.all(this.aliases.map(alias => {
let worker = addAlias(alias.name, redirectTo, artistIdOrName ? artistId : undefined).then(wait);
if (alias.tgm.aliasUsed) worker = worker.then(() => findArtistAlias(redirectTo, artistId))
.then(newAlias => alias.tgm.addAliasToGroups(newAlias.name));
return worker;
})))) : Promise.resolve('No dependants');
}
}
class ArtistGroupKeeper {
constructor() {
for (let torrentGroup of artist.torrentgroup) if (torrentGroup.extendedArtists) for (let importance in torrentGroup.extendedArtists) {
const artists = torrentGroup.extendedArtists[importance];
if (Array.isArray(artists) && artists.length > 0) continue;
this.artistId = artist.id;
this.aliasName = `__${artist.id.toString()}__${Date.now().toString(16)}`;
this.groupId = torrentGroup.groupId;
this.importance = parseInt(importance);
this.locked = false;
return this;
}
throw 'Unable to find a spare group';
}
hold() {
if (this.locked) return Promise.reject('Not available');
if (!this.groupId) throw 'Unable to find a spare group';
this.locked = true;
return addAlias(this.aliasName).then(wait)
.then(() => addAliasToGroup(this.groupId, this.aliasName, [this.importance]));
}
release(artistIdOrName = this.artistId) {
if (!this.locked) return Promise.reject('Not available');
return resolveArtistId(artistIdOrName).then(artistId =>
deleteArtistFromGroup(this.groupId, artistId, [this.importance]).then(wait)
.then(() => deleteAlias(this.aliasName, artistIdOrName && artistId))).then(() => { this.locked = false });
}
}
function getSelectedRedirect(defaultsToMain = false) {
let redirect = aliasesRoot.querySelector('select[name="redirect"]');
if (redirect == null) throw 'Assertion failed: can not locate redirect selector';
redirect = {
id: parseInt(redirect.options[redirect.selectedIndex].value),
name: redirect.options[redirect.selectedIndex].label,
};
console.assert(redirect.id >= 0 && redirect.name, 'redirect.id >= 0 && redirect.name');
if (defaultsToMain && redirect.id == 0) {
redirect.id = mainIdentityId;
redirect.name = artist.name;
}
return Object.freeze(redirect);
}
function failHandler(reason) {
if (activeElement instanceof HTMLElement && activeElement.parentNode != null) {
activeElement.style.color = null;
if (activeElement.dataset.caption) activeElement.value = activeElement.dataset.caption;
activeElement.disabled = false;
activeElement = null;
inProgress = false;
}
alert(reason);
}
// Damage control
function setRecoveryInfo(action, aliases, param) {
console.assert(aliases && (typeof aliases == 'object' || Array.isArray(aliases)) && action,
"aliases && (typeof aliases == 'object' || Array.isArray(aliases)) && action");
const damageControl = {
artist: artist,
action: action,
aliases: aliases,
};
if (param) damageControl.param = param;
GM_setValue('damage_control', damageControl);
}
function recoverFromFailure() {
const recoveryInfo = GM_getValue('damage_control');
if (!recoveryInfo) return Promise.reject('No unfinished operation present');
if (recoveryInfo.artist.id != artist.id)
return Promise.reject('Unfinished operation for this artist not present');
//artist = recoveryInfo.artist; // ?
return eval(recoveryInfo.action)(recoveryInfo.artist, aliases, recoveryInfo.param).then(clearRecoveryInfo);
}
function isBadRDA(alias) {
if (!alias) throw 'Invalid argument';
if (!alias.redirectId) return false; //throw 'Not a redirecting alias';
if (alias.tgm.aliasUsed) return true;
const target = findAlias(alias.redirectId, true);
return !target || target.id != alias.redirectId;
}
function dupesCleanup(alias) {
if (!alias || !alias.id) throw 'Invalid argument';
const target = findAlias(alias.redirectId) || alias, workers = [ ], dupes = [ ];
aliases.forEach(function(li) {
const dupe = getAlias(li);
if (!dupe || dupe.id == alias.id || dupe.name.toLowerCase() != alias.name.toLowerCase()) return;
let index;
const ancestor = findAlias(dupe.redirectId);
if (ancestor && 'dependants' in ancestor && ancestor.dependants.hasDependants) {
while ((index = ancestor.dependants.aliases.indexOf(alias => alias.id == dupe.id)) >= 0)
ancestor.dependants.aliases.splice(index, 1);
}
if ('dependants' in dupe && dupe.dependants.hasDependants) {
while ((index = dupe.dependants.aliases.indexOf(dependant => dependant.name.toLowerCase() == alias.name.toLowerCase())) >= 0)
dupe.dependants.aliases.splice(index, 1);
if (dupe.dependants.hasDependants) workers.push(dupe.dependants.removeAll());
}
workers.push(dupe.tgm.aliasUsed ? dupe.tgm.removeAliasFromGroups().then(() => deleteAlias(dupe.id))
: deleteAlias(dupe.id));
dupes.push(dupe);
});
return workers.length > 0 ? Promise.all(workers).then(() => Promise.all(dupes.map(function(dupe) {
const workers = [ ];
if (dupe.tgm.aliasUsed) workers.push(dupe.tgm.addAliasToGroups(target.name)
.then(() => { Object.assign(target.tgm.groups, dupe.tgm.groups) }));
if ('dependants' in dupe && dupe.dependants.hasDependants) workers.push(dupe.dependants.restoreAll(target.id)
.then(wait).then(() => Promise.all(dupe.dependants.aliases.map(alias => resolveAliasId(alias.name, artist.id)
.then(aliasId => (alias.id = aliasId, alias))))).then(function(aliases) {
if (!('dependants' in target)) target.dependants = new AliasDependantsManager(target.id);
if (!Array.isArray(target.dependants.aliases)) target.dependants.aliases = [ ];
Array.prototype.push.apply(target.dependants.aliases, aliases);
}));
if (workers.length > 0) return Promise.all(workers);
}))) : Promise.resolve('No duplicate aliases');
}
function prologue(alias, agk) {
let worker = dupesCleanup(alias).then(function() {
const workers = [ ];
if (agk && alias.tgm.size >= artist.torrentgroup.length) workers.push(agk.hold());
if ('dependants' in alias && alias.dependants.hasDependants) workers.push(alias.dependants.removeAll());
if (workers.length > 0) return Promise.all(workers);
});
if (alias.tgm.aliasUsed) worker = worker.then(() => alias.tgm.removeAliasFromGroups());
return worker;
}
function epilogue(alias, agk, id1, id2 = id1) {
function finish() {
const workers = [ ];
if ('dependants' in alias && alias.dependants.hasDependants) workers.push(alias.dependants.restoreAll(id2));
if (agk && alias.tgm.size >= artist.torrentgroup.length) workers.push(agk.release());
return Promise.all(workers);
}
if (!alias || !id1) throw 'Invalid argument';
return alias.tgm.aliasUsed ? alias.tgm.addAliasToGroups(id1).then(finish) : finish();
}
function redirectAliasTo(alias, redirectIdOrName) {
if (!alias) throw 'Invalid argument';
return resolveAliasId(redirectIdOrName, -1, true).then(function(redirectId) {
if (redirectId == alias.id) return Promise.reject('Alias can\'t redirect to itself');
if (alias.redirectId == redirectId) return Promise.resolve('Redirect doesnot change');
const agk = new ArtistGroupKeeper, workers = [ ];
if (alias.tgm.size >= artist.torrentgroup.length) workers.push(agk.hold());
if ('dependants' in alias && alias.dependants.hasDependants) workers.push(alias.dependants.removeAll());
let worker = Promise.all(workers);
if (alias.tgm.aliasUsed) worker = worker.then(() => alias.tgm.removeAliasFromGroups());
return worker.then(() => deleteAlias(alias.id)).then(wait)
.then(() => addAlias(alias.name, redirectId)).then(wait)
.then(() => epilogue(alias, agk, alias.name, redirectId));
});
}
function renameAlias(alias, newName) {
if (!alias) throw 'Invalid argument';
const agk = new ArtistGroupKeeper;
return prologue(alias, agk).then(() => deleteAlias(alias.id)).then(() => wait(newName).then(addAlias))
.then(wait).then(() => epilogue(alias, agk, newName));
}
function resolveRDA(alias) {
if (!alias || !alias.id || !alias.redirectId) return Promise.reject('Invalid argument');
if (!isBadRDA(alias)) return Promise.resolve('Alias fully resolved');
const target = findAlias(alias.redirectId, true);
return alias.tgm.removeAliasFromGroups().then(() => alias.tgm.addAliasToGroups((target || artist).name)).then(function() {
if (target && target.tgm) Object.assign(target.tgm.groups, alias.tgm.groups);
alias.tgm.groups = { };
//if (!target) return deleteAlias(alias.id);
if (!target || alias.redirectId != target.id)
return deleteAlias(alias.id).then(() => addAlias(alias.name, target ? target.id : mainIdentityId));
});
}
const recoveryQuestion = `Last operation for current artist was not successfull,
if you continue, recovery information will be invalidated or lost.`;
// NRA actions
function makeItMain(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
console.assert(alias.id != mainIdentityId, 'alias.id != mainIdentityId');
let nagText = `CAUTION
This action makes alias "${alias.name}" the main identity for artist ${artist.name},
while "${artist.name}" becomes it's subordinate N-R alias.`;
if (alias.tgm.aliasUsed) nagText += '\n\nBlocked by ' + alias.tgm.size + ' groups';
if (!confirm(nagText + epitaph)) return false;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
const agk = new ArtistGroupKeeper;
if (alias.tgm.aliasUsed) prologue(alias, agk).then(() => deleteAlias(alias.id)).then(wait)
.then(() => alias.tgm.addAliasToGroups(alias.name)).then(() => findArtistId(alias.name))
.then(function(newArtistId) {
let worker = changeArtistId(newArtistId).then(wait);
if (alias.tgm.size >= artist.torrentgroup.length) worker = worker.then(() => agk.release(newArtistId));
if ('dependants' in alias && alias.dependants.hasDependants)
worker = worker.then(() => alias.dependants.restoreAll(alias.name, newArtistId));
return worker.then(function() {
let body = document.getElementById('body');
body = body != null && body.value.trim() || artist.body;
let image = document.body.querySelector('input[type="text"][name="image"]');
image = image != null && image.value.trim() || artist.image;
const workers = [ ];
if (body || image) workers.push(editArtist(image, body, 'Wiki transfer (AAM)', newArtistId));
const similarArtists = artist.similarArtists ?
artist.similarArtists.map(similarArtist => similarArtist.name) : [ ];
if (similarArtists.length > 0) workers.push(addSimilarArtists(similarArtists, newArtistId)
.then(() => { console.log(`${similarArtists.length} similar artists were transfered to new id`) }));
if (workers.length > 0) return Promise.all(workers);
}).then(() => { gotoArtistEditPage(newArtistId) });
}).catch(failHandler);
else {
const mainIdentity = findAlias(mainIdentityId);
console.assert(mainIdentity != null, 'mainIdentity != null');
let worker = dupesCleanup(mainIdentity).then(function() {
const workers = [mainIdentity, alias].filter(alias => 'dependants' in alias && alias.dependants.hasDependants)
.map(alias => alias.dependants.removeAll());
if (mainIdentity.tgm.size >= artist.torrentgroup.length) workers.push(agk.hold());
if (workers.length > 0) return Promise.all(workers);
});
if (mainIdentity.tgm.aliasUsed) worker = worker.then(() => mainIdentity.tgm.removeAliasFromGroups());
worker = worker.then(() => renameArtist(alias.name)).then(wait)
.then(() => deleteAlias(artist.name)).then(wait).then(() => addAlias(artist.name)).then(wait);
if (mainIdentity.tgm.aliasUsed) worker = worker.then(() => mainIdentity.tgm.addAliasToGroups(artist.name));
if (mainIdentity.tgm.size >= artist.torrentgroup.length) worker = worker.then(() => agk.release());
worker = worker.then(function() {
const workers = [ ];
if ('dependants' in alias && alias.dependants.hasDependants)
workers.push(alias.dependants.restoreAll(alias.name));
if ('dependants' in mainIdentity && mainIdentity.dependants.hasDependants)
workers.push(mainIdentity.dependants.restoreAll(artist.name));
if (workers.length > 0) return Promise.all(workers);
}).then(() => { document.location.reload() }, failHandler);
}
return false;
}
function _redirectNRA(currentTarget, redirect) {
const alias = getAlias(currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (redirect.id == alias.id) return;
let nagText = `CAUTION
This action makes alias "${alias.name}" redirect to artist\'s variant "${redirect.name}",
and replaces the alias in all involved groups (if any) with this variant.`;
if (alias.tgm.aliasUsed) nagText += '\n\nBlocked by ' + alias.tgm.size + ' groups';
if (!confirm(nagText + epitaph)) return;
inProgress = true;
activeElement = currentTarget;
currentTarget.textContent = 'processing ...';
currentTarget.style.color = 'red';
redirectAliasTo(alias, redirect.id).then(() => { document.location.reload() }, failHandler);
}
function changeToRedirect(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
_redirectNRA(evt.currentTarget, getSelectedRedirect(true));
return false;
}
function renameNRA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
let nagText = `CAUTION
This action renames alias
"${alias.name}",
and replaces the alias in all involved groups (if any) with the new name.
New name can't be artist name or alias already taken on the site.`;
if (alias.tgm.aliasUsed) nagText += '\n\nBlocked by ' + alias.tgm.size + ' groups';
let newName = prompt(nagText + '\n\nThe operation can be reverted only by hand, to proceed enter and confirm new name\n\n', alias.name);
if (newName) newName = newName.trim(); else return false;
if (!newName || newName == alias.name) return false;
const currentTarget = evt.currentTarget;
(newName.toLowerCase() == alias.name.toLowerCase() ? Promise.reject('Case change')
: findArtistAlias(newName, 0, true).then(function(alias) {
_redirectNRA(currentTarget, alias);
//alert(`Name is already taken by alias id ${alias.id}, the operation is aborted`);
}, reason => getSiteArtist(newName).then(function(dupeTo) {
siteArtistsCache[dupeTo.name] = dupeTo;
alert(`Name is already taken by artist "${dupeTo.name}" (${dupeTo.id}), the operation is aborted`);
GM_openInTab(document.location.origin + '/artist.php?id=' + dupeTo.id.toString(), false);
}))).catch(function(reason) {
inProgress = true;
activeElement = currentTarget;
currentTarget.textContent = 'processing ...';
currentTarget.style.color = 'red';
//setRecoveryInfo('renameAlias', alias, newName);
renameAlias(alias, newName).then(() => { document.location.reload() }, failHandler);
});
return false;
}
function cutOffNRA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
console.assert('dependants' in alias, "'dependants' in alias");
let nagText = 'CAUTION\n\nThis action ';
nagText += alias.tgm.aliasUsed ? `cuts off identity "${alias.name}"
from artist ${artist.name} and leaves it in separate group.
Blocked by ${alias.tgm.size} groups`
: `deletes identity "${alias.name}" and all it's dependants (${alias.dependants.size}).
(Not used in any release)`;
if (artist.torrentgroup.length <= alias.tgm.size) nagText += `
This action also vanishes this artist group as no other name variants
are used in any release`;
if (!confirm(nagText + epitaph)) return false;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
const agk = new ArtistGroupKeeper;
let worker = prologue(alias, agk).then(() => deleteAlias(alias.id)).then(wait);
if (alias.tgm.aliasUsed) worker = worker.then(() => alias.tgm.addAliasToGroups(alias.name));
worker = worker.then(alias.tgm.aliasUsed ? () => findArtistId(alias.name).then(function(newArtistId) {
let worker = Promise.resolve();
if ('dependants' in alias && alias.dependants.hasDependants)
worker = worker.then(() => alias.dependants.restoreAll(alias.name, newArtistId));
return worker.then(function() {
if (artist.torrentgroup.length > alias.tgm.size) {
GM_openInTab(document.location.origin + '/artist.php?id=' + newArtistId.toString(), true);
document.location.reload();
} else gotoArtistPage(newArtistId);
});
}) : () => { document.location.reload() }).catch(failHandler);
return false;
}
function split(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
let newName, newNames = [ ];
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (!alias.tgm.aliasUsed) return false;
const prologue = () => {
let result = `CAUTION
This action splits artist's identity "${alias.name}" into two or more names
and replaces the identity in all involved groups with new names. No linking of new names
to current artist will be performed, profile pages of names that aren't existing aliases already
will open in separate tabs for review.`;
if (alias.tgm.aliasUsed) result += '\n\nBlocked by ' + alias.tgm.size + ' groups';
if (newNames.length > 0) result += '\n\n' + newNames.map(n => '\t' + n).join('\n');
return result;
};
do {
if ((newName = prompt(prologue().replace(/^CAUTION\s*/, '') +
`\n\nEnter carefully new artist name #${newNames.length + 1}, to finish submit empty input\n\n`,
newNames.length < 2 ? alias.name : undefined)) == undefined) return false;
if ((newName = newName.trim()) && !newNames.includes(newName)) newNames.push(newName);
} while (newName);
if (newNames.length < 2 || !confirm(prologue() + epitaph)) return false;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
console.info(alias.name, 'present in these groups:', alias.tgm.groups);
function openInTab(artistId) {
if (artistId > 0) GM_openInTab(document.location.origin + '/artist.php?id=' + artistId.toString(), true);
}
//alias.dependants.removeAll();
alias.tgm.removeAliasFromGroups().then(() => deleteAlias(alias.id))
.then(() => Promise.all(newNames.map(TorrentGroupsManager.prototype.addAliasToGroups.bind(alias.tgm))))
.then(() => findArtistId(newNames[0]).then(function(artistId) {
if (artistId != artist.id && artist.torrentgroup.length > alias.tgm.size) openInTab(artistId);
newNames.slice(1).forEach(newName => { findArtistId(newName).then(openInTab) });
if (artistId == artist.id) document.location.reload(); else gotoArtistPage(artistId);
})).catch(failHandler);
return false;
}
function select(evt) {
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
console.assert(dropDown instanceof HTMLSelectElement, 'dropDown instanceof HTMLSelectElement');
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (!alias.redirectId && dropDown != null) {
dropDown.value = alias.id;
if (typeof dropDown.onchange == 'function') dropDown.onchange();
}
return false;
}
// RDA actions
function changeToNra(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (!confirm(`This action makes artist's identity "${alias.name}" distinct`)) return false;
console.assert(alias && typeof alias == 'object');
inProgress = true;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
let worker = alias.tgm.aliasUsed ? alias.tgm.removeAliasFromGroups() : Promise.resolve();
worker = worker.then(() => deleteAlias(alias.id)).then(() => wait(alias.name).then(addAlias));
if (alias.tgm.aliasUsed) worker = worker.then(() => alias.tgm.addAliasToGroups(alias.name));
worker = worker.then(() => { document.location.reload() }, failHandler);
return false;
}
function changeRedirectRDA(currentTarget, alias, target) {
console.assert(currentTarget instanceof HTMLAnchorElement, 'currentTarget instanceof HTMLAnchorElement');
console.assert(alias && alias.id > 0, 'alias && alias.id > 0');
console.assert(target && target.id > 0, 'target && target.id > 0');
inProgress = true;
activeElement = currentTarget;
currentTarget.textContent = 'processing ...';
currentTarget.style.color = 'red';
(alias.tgm.aliasUsed ? resolveRDA(alias) : Promise.resolve('Resolved'))
.then(() => redirectAliasTo(alias, target.id)).then(() => { document.location.reload() }, failHandler);
}
function changeRedirect(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode), redirect = getSelectedRedirect();
console.assert(alias && typeof alias == 'object');
if (redirect.id == 0) return changeToNra(evt); else if (redirect.id == alias.redirectId) return false;
if (confirm(`This action changes alias "${alias.name}"'s to resolve to "${redirect.name}"`))
changeRedirectRDA(evt.currentTarget, alias, redirect);
return false;
}
function renameRDA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
console.assert(alias.redirectId, 'alias.redirectId');
let newName = prompt(`This action renames alias "${alias.name}"`, alias.name);
if (newName) newName = newName.trim(); else return false;
if (!newName || newName == alias.name) return false;
const currentTarget = evt.currentTarget;
findArtistAlias(newName, 0, true).then(function(target) {
if (target.id != alias.id && target.id != alias.redirectId) changeRedirectRDA(currentTarget, alias, target);
else alert(`Name is already taken by alias id ${alias.id}, the operation is aborted`);
}, reason => getSiteArtist(newName).then(function(dupeTo) {
siteArtistsCache[dupeTo.name] = dupeTo;
alert(`Name is already taken by artist "${dupeTo.name}" (${dupeTo.id}), the operation is aborted`);
GM_openInTab(document.location.origin + '/artist.php?id=' + dupeTo.id.toString(), false);
})).catch(function(reason) {
inProgress = true;
activeElement = currentTarget;
currentTarget.textContent = 'processing ...';
currentTarget.style.color = 'red';
renameAlias(alias, newName).then(() => { document.location.reload() }, failHandler);
});
return false;
}
function fixRDA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (!alias.redirectId) return false;
const target = findAlias(alias.redirectId, true);
if (!target) throw 'Assertion failed: redirecting alias was not found';
if (!confirm(`This action forces alias "${alias.name}"'s to resolve to "${target.name}" in all still linked releases.`))
return false;
inProgress = true;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
resolveRDA(alias).then(() => { document.location.reload() }, failHandler);
return false;
}
function X(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return false;
console.assert(evt.currentTarget.nodeName == 'A', "evt.currentTarget.nodeName == 'A'");
const alias = getAlias(evt.currentTarget.parentNode);
console.assert(alias && typeof alias == 'object');
if (!confirm('Delete this alias?')) return false;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'red';
deleteAlias(alias.id).then(() => { document.location.reload() }, failHandler);
return false;
}
// batch actions
function batchAction(actions, condition, onlySelected = true) {
console.assert(typeof actions == 'function', "typeof actions == 'function'");
if (typeof actions != 'function') throw 'Invalid argument';
if (onlySelected) {
var selAliases = aliasesRoot.querySelectorAll('div > ul > li > input.aam[type="checkbox"]:checked');
if (selAliases.length <= 0) return Promise.reject('No aliases selected');
selAliases = Array.from(selAliases).map(checkbox => getAlias(checkbox.parentNode));
} else selAliases = Array.from(aliases).map(getAlias).filter(alias => alias.id != mainIdentityId);
console.assert(selAliases.every(Boolean), 'selAliases.every(Boolean)', selAliases);
if (!selAliases.every(Boolean)) throw 'Assertion failed: element(s) without linked alias';
if (typeof condition == 'function') selAliases = selAliases.filter(condition);
if (selAliases.length <= 0) return Promise.reject('No alias fulfils for this action');
//setRecoveryInfo('batchRecovery', selAliases, actions.toString());
let worker = alias => dupesCleanup(alias).then(() => actions(alias));
return (artist.torrentgroup.every(torrentGroup => selAliases.some(alias =>
alias.tgm && Object.keys(alias.tgm).includes(torrentGroup.groupId))) ? (function() {
const agk = new ArtistGroupKeeper;
return agk.hold().then(() => Promise.all(selAliases.map(worker))).then(() => agk.release());
})() : Promise.all(selAliases.map(actions))).then(function() {
clearRecoveryInfo();
document.location.reload();
}, failHandler);
}
function batchRecovery(artist, aliases, actions) {
if (typeof actions == 'string') actions = eval(actions);
if (typeof actions != 'function') return Promise.reject('Action not valid callback');
console.assert(Array.isArray(aliases) && aliases.length > 0, 'Array.isArray(aliases) && aliases.length > 0');
return Promise.all(aliases.map(actions));
}
function batchChangeToRDA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return;
const redirect = getSelectedRedirect();
if (redirect.id == 0) return batchChangeToNRA(evt);
let nagText = `CAUTION
This action makes all selected aliases redirect to artist\'s variant
"${redirect.name}",
and replaces all non-redirect aliases in their involved groups (if any) with this variant.`;
if (!confirm(nagText + epitaph)) return false;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.disabled = true;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'goldenrod';
batchAction(alias => redirectAliasTo(alias, redirect.id), alias => alias.id != redirect.id).catch(failHandler);
}
function batchChangeToNRA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return;
let nagText = `This action makes all selected RDAs distinct within artist`;
if (!confirm(nagText)) return;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.disabled = true;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'goldenrod';
batchAction(alias => deleteAlias(alias.id).then(() => wait(alias.name).then(addAlias)),
alias => alias.redirectId > 0).catch(failHandler);
}
function batchRemove(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return;
let nagText = `This action deletes all selected RDAs and unused NRAs`;
if (!confirm(nagText)) return;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'goldenrod';
batchAction(alias => deleteAlias(alias.id), alias => alias.redirectId > 0 || !alias.tgm.aliasUsed)
.catch(failHandler);
}
function batchFixRDA(evt) {
if (inProgress || hasRecoveryInfo() && !confirm(recoveryQuestion)) return;
// let nagText = `This action fixes all broken redirecting aliases`;
// if (!confirm(nagText)) return;
inProgress = true;
activeElement = evt.currentTarget;
evt.currentTarget.textContent = 'processing ...';
evt.currentTarget.style.color = 'goldenrod';
batchAction(alias => resolveRDA(alias), isBadRDA, false).catch(failHandler);
}
if (hasRecoveryInfo()) for (let h2 of document.body.querySelectorAll('div#content h2')) {
if (!h2.textContent.includes('Artist aliases')) continue;
let input = document.createElement('INPUT');
input.type = 'button';
input.dataset.caption = input.value = 'Recover from unfinished operation';
window.tooltipster.then(() => { input.tooltipster({
content: 'Unfinished operation information was found for this artist
Recovery will try to finish.',
}) });
input.style.marginLeft = '2em';
input.value = '[ processing ... ]';
input.onclick = function(evt) {
if (inProgress || !confirm('This will try to finalize last interrputed operation. Continue?')) return;
(activeElement = evt.currentTarget).disabled = inProgress = true;
activeElement.style.color = 'goldenrod';
recoverFromFailure().then(function() {
activeElement.value = 'Recovery successfull, reloading...';
//document.location.reload();
}, function(reason) {
activeElement.style.color = null;
activeElement.value = input.dataset.caption;
activeElement.disabled = false;
alert('Recovery was not successfull: ' + reason);
document.location.reload();
});
};
h2.insertAdjacentElement('afterend', input);
}
for (let li of aliases) if (!rdExtractor.test(li.textContent)) {
const alias = {
id: li.querySelector(':scope > span:nth-of-type(1)'),
name: li.querySelector(':scope > span:nth-of-type(2)'),
};
if (Object.keys(alias).some(key => key == null) || !(alias.id = parseInt(alias.id.textContent))) continue;
const elem = alias.name;
if (!(alias.name = alias.name.textContent) || alias.name != artist.name) continue;
mainIdentityId = alias.id;
rmDelLink(li);
elem.style.fontWeight = 900;
break;
}
console.assert(mainIdentityId > 0, 'mainIdentityId > 0');
function applyDynaFilter(str) {
const filterById = Number.isInteger(str), norm = str => str.toLowerCase();
if (!filterById) str = str ? /^\d+$/.test(str) && parseInt(str) || (function() {
const rx = /^\s*\/(.+)\/([dgimsuy]+)?\s*$/i.exec(str);
if (rx != null) try { return new RegExp(...rx) } catch(e) { /*console.info(e)*/ }
})() || norm(str.trim()) : undefined;
function isHidden(li) {
if (!str) return false;
let elem = li.querySelector(':scope > span:nth-of-type(2)');
console.assert(elem != null, 'elem != null');
if (!filterById && (elem == null || (str instanceof RegExp ? str.test(elem.textContent)
: norm(elem.textContent).includes(str)))) return false;
if (!Number.isInteger(str)) return true;
elem = li.querySelector(':scope > span:nth-of-type(1)');
if (elem != null && str == parseInt(elem.textContent)) return false;
return (elem = rdExtractor.exec(li.textContent)) == null || str != parseInt(elem[1]);
}
for (let li of aliases) li.hidden = isHidden(li);
}
for (let li of aliases) {
li.alias = {
id: li.querySelector(':scope > span:nth-of-type(1)'),
name: li.querySelector(':scope > span:nth-of-type(2)'),
redirectId: rdExtractor.exec(li.textContent),
};
if (li.alias.id == null || li.alias.name == null) {
delete li.alias;
continue;
}
if (li.alias.redirectId == null) {
li.alias.id.style.cursor = 'pointer';
li.alias.id.onclick = function(evt) {
const aliasId = parseInt(evt.currentTarget.textContent);
console.assert(aliasId >= 0, 'aliasId >= 0');
if (!(aliasId >= 0)) throw 'Invalid node value';
applyDynaFilter(aliasId);
const dynaFilter = document.getElementById('aliases-dynafilter');
if (dynaFilter != null) dynaFilter.value = aliasId;
};
li.alias.id.title = 'Click to filter';
(elem => { window.tooltipster.then(() => { $(elem).tooltipster() }) })(li.alias.id);
}
if (!(li.alias.id = parseInt(li.alias.id.textContent)) || !(li.alias.name = li.alias.name.textContent)) continue; // assertion failed
li.alias.tgm = new TorrentGroupsManager(li.alias.id);
let buttonIndex = 0;
function addButton(caption, tooltip, cls, callback, highlight = false) {
const a = document.createElement('A');
a.className = 'brackets';
if (cls) a.classList.add(cls);
a.style.marginLeft = buttonIndex > 0 ? '5pt' : '3pt';
if (highlight) a.style.color = 'red';
a.href = '#';
if (caption) a.dataset.caption = a.textContent = caption.toUpperCase();
if (tooltip) window.tooltipster.then(() => { $(a).tooltipster({ content: tooltip.replace(/\r?\n/g, '
') }) }).catch(function(reason) {
a.title = tooltip;
console.warn(reason);
});
if (typeof callback == 'function') a.onclick = callback;
li.append(a);
++buttonIndex;
}
if (li.alias.redirectId != null) { // RDA
li.alias.redirectId = parseInt(li.alias.redirectId[1]);
console.assert(li.alias.redirectId > 0, 'li.alias.redirectId > 0');
for (let span of li.getElementsByTagName('SPAN')) if (parseInt(span.textContent) == li.alias.redirectId) {
const deref = findAlias(li.alias.redirectId);
if (deref) window.tooltipster.then(function() {
const tooltip = '' + deref.name + '';
if ($(span).data('plugin_tooltipster'))
$(span).tooltipster('update', tooltip).data('plugin_tooltipster').options.delay = 100;
else $(span).tooltipster({ delay: 100, content: tooltip });
}).catch(function(reason) {
//span.textContent = deref + ' (' + span.textContent + ')';
span.title = deref.name;
console.warn(reason);
});
span.style.cursor = 'pointer';
span.onclick = function(evt) {
applyDynaFilter(li.alias.redirectId);
const dynaFilter = document.getElementById('aliases-dynafilter');
if (dynaFilter != null) dynaFilter.value = li.alias.redirectId;
};
}
addButton('NRA', 'Change to non-redirecting alias', 'make-nra', changeToNra);
addButton('CHG', 'Change redirect', 'redirect-to', changeRedirect);
addButton('RN', 'Rename this alias', 'rename', renameRDA);
if (isBadRDA(li.alias)) {
li.style.backgroundColor = '#FF000020';
addButton('FIX', 'This alias is still linked to torrent groups, doesn\'t reolve to true alias or resolves to non-existing alias. Fix forces resolve the alias to it\'s true target, aliases redirecting to invalid id will resolve to main artist name.',
'fix-rda', fixRDA, true);
}
} else { // NRA
delete li.alias.redirectId;
li.style.color = isLightTheme ? 'peru' : isDarkTheme ? 'antiquewhite' : 'darkorange';
if (li.alias.name != artist.name) {
addButton('MAIN', 'Make this alias main artist\'s identity', 'make-main', makeItMain);
addButton('RD', 'Change to redirecting alias to artist\'s identity selected in dropdown below',
'redirect-to', changeToRedirect);
addButton('RN', 'Rename this alias while keeping it distinguished from the main identity',
'rename', renameNRA);
addButton('CUT', 'Just unlink this alias from the artist and leave it in separate group; unused aliases will be deleted',
'cut-off', cutOffNRA);
}
if (li.alias.tgm.aliasUsed) addButton('S', 'Split this ' + (li.alias.name == artist.name ? 'artist': 'alias') +
' to two or more names', 'split', split);
addButton('SEL', 'Select as redirect target', 'select', select);
}
if (li.alias.tgm.aliasUsed) {
rmDelLink(li);
const span = document.createElement('span');
span.textContent = '(' + li.alias.tgm.size + ')';
span.style.marginLeft = '5pt';
if (li.alias.redirectId > 0) span.style.color = 'red';
window.tooltipster.then(() => { $(span).tooltipster({ content: 'Amount of groups blocking this alias' }) }).catch(function(reason) {
span.title = 'Amount of groups blocking this alias';
console.warn(reason);
});
li.append(span);
}
for (let a of li.getElementsByTagName('A')) if (a.textContent.trim() == 'X') {
a.href = '#';
a.dataset.caption = a.textContent;
a.onclick = X;
a.style.marginLeft = '3pt';
}
for (let a of li.getElementsByTagName('A')) if (a.textContent.trim() == 'User') {
const href = new URL(a.href);
if (userId > 0 && parseInt(href.searchParams.get('id')) == userId) {
const span = document.createElement('SPAN');
span.className = 'brackets';
span.style.color = 'skyblue';
span.textContent = 'Me';
li.replaceChild(span, a);
}
}
}
for (let li of aliases) if ('alias' in li && !(li.alias.redirectId > 0))
li.alias.dependants = new AliasDependantsManager(li.alias.id);
const h3 = aliasesRoot.getElementsByTagName('H3');
if (h3.length > 0 && aliases.length > 1) {
const elems = createElements('LABEL', 'INPUT', 'INPUT', 'DIV', 'LABEL', 'INPUT', 'IMG', 'SPAN');
elems[3].style = 'transition: height 0.5s; height: 0; overflow: hidden;';
elems[3].id = 'batch-controls';
elems[4].style = 'margin-left: 15pt; padding: 5pt; line-height: 0;';
elems[5].type = 'checkbox';
elems[5].onclick = function(evt) {
for (let input of aliasesRoot.querySelectorAll('div > ul > li > input.aam[type="checkbox"]'))
if (!input.parentNode.hidden) input.checked = evt.currentTarget.checked;
};
elems[4].append(elems[5]);
elems[3].append(elems[4]);
function addButton(caption, callback, tooltip, margin = '5pt', highlight = false) {
const input = document.createElement('INPUT');
input.type = 'button';
if (caption) input.dataset.caption = input.value = caption;
if (margin) input.style.marginLeft = margin;
if (highlight) input.style.color = 'red';
if (tooltip) window.tooltipster.then(() => { $(input).tooltipster({ content: tooltip.replace(/\r?\n/g, '
') }) })
.catch(reason => { console.warn(reason) });
if (typeof callback == 'function') input.onclick = callback;
elems[3].append(input);
}
addButton('Redirect', batchChangeToRDA, 'Make selected aliases redirect to selected identity', '1em');
addButton('Distinct', batchChangeToNRA, 'Make selected aliases distinct (make them NRA)');
addButton('Delete', batchRemove, 'Remove all selected aliases (except used NRAs)');
if (aliasesRoot.querySelector('a.fix-rda') != null)
addButton('Fix broken RDAs', batchFixRDA, 'Fixes all broken redirecting aliases; aliases resolving to non-existing id will resolve to main identity', undefined, true);
h3[0].insertAdjacentElement('afterend', elems[3]);
elems[2].type = 'button';
elems[2].value = 'Show batch controls';
elems[2].style.marginLeft = '2em';
elems[2].onclick = function(evt) {
if ((elems[3] = document.getElementById('batch-controls')) != null) elems[3].style.height = 'auto';
evt.currentTarget.remove();
let tabIndex = 0;
for (let li of aliasesRoot.querySelectorAll('div > ul > li')) {
let elem = li.querySelector(':scope > span:nth-of-type(2)');
if (elem == null || elem.textContent == artist.name) continue;
elem = document.createElement('INPUT');
elem.type = 'checkbox';
elem.className = 'aam';
elem.tabIndex = ++tabIndex;
elem.style = 'margin-right: 2pt; position: relative; left: -2pt;';
li.prepend(elem);
li.style.listStyleType = 'none';
}
};
h3[0].insertAdjacentElement('afterend', elems[2]);
elems[0].textContent = 'Filter by';
elems[1].type = 'text';
elems[1].id = 'aliases-dynafilter';
elems[1].style = 'margin-left: 1em; width: 20em; padding-right: 20pt;';
elems[1].ondblclick = evt => { applyDynaFilter(evt.currentTarget.value = '') };
elems[1].oninput = evt => { applyDynaFilter(evt.currentTarget.value) };
elems[1].ondragover = elems[1].onpaste = evt => { evt.currentTarget.value = '' };
elems[6].height = 17;
elems[6].style = 'position: relative; left: -18pt; top: 2pt;';
elems[6].src = GM_getResourceURL('input-clear-button'); //'https://ptpimg.me/d005eu.png';
elems[6].onclick = evt => {
applyDynaFilter();
const input = document.getElementById('aliases-dynafilter');
if (input != null) input.value = '';
};
elems[0].append(elems[1]);
elems[0].append(elems[6]);
h3[0].insertAdjacentElement('afterend', elems[0]);
elems[7].textContent = '(' + aliases.length + ')';
elems[7].style = 'margin-left: 1em; font: normal 9pt Helvetica, Arial, sans-serif;';
h3[0].append(elems[7]);
}
if (dropDown != null) dropDown.onchange = function(evt) {
const redirectId = parseInt((evt instanceof Event ? evt.currentTarget : dropDown).value);
if (!(redirectId >= 0)) throw 'Unknown selection';
for (let li of aliases) {
const alias = getAlias(li);
if (alias == null || alias.redirectId > 0) continue;
li.style.backgroundColor = alias.id == redirectId ?
isLightTheme ? '#ffde004d' : isDarkTheme ? 'darkslategray' : 'orange' : null;
}
};
if (typeof dropDown.onchange == 'function') dropDown.onchange();
function addDiscogsImport() {
const dcEntryTypes = {
a: 'artist',
r: 'release',
m: 'master',
l: 'label',
u: 'users',
};
const sitesFilter = url =>
url && !siteBlacklist.some(pattern => url.toLowerCase().includes(pattern.toLowerCase()));
const dcArtistLink = artist =>
`[align=left][url=${artist.uri}][img]https://ptpimg.me/v27891.png[/img][/url][/align]`;
const useLinkFriendlyNames = GM_getValue('discogs_friendly_urls', false);
function dcUrlToBB(url) {
if (!url || !(url = url.trim())) return null;
let friendlyName = /^(.+?):\s*(https?:\/\/.+)$/i.exec(url);
if (friendlyName != null) {
url = friendlyName[2];
friendlyName = friendlyName[1];
} else try {
const _url = new URL(url);
if (!['https:', 'http:'].includes(_url.protocol)) throw 'Unsupported protocol';
for (let entry of Object.entries({
'Discogs': 'discogs.com', 'Bandcamp': '.bandcamp.com', 'SoundCloud': 'soundcloud.com',
'Last.fm': 'last.fm', 'YouTube': 'youtube.com', 'Wikipedia': 'wikipedia.org', 'IMDb': 'imdb.com',
'MusicBrainz': 'musicbrainz.org', 'Spotify': 'spotify.com', 'Tidal': 'tidal.com',
'Tumblr': 'tumblr.com', 'Twitter': 'twitter.com', 'Facebook': 'facebook.com',
})) if (_url.hostname.endsWith(entry[1])) friendlyName = entry[0];
} catch(e) {
console.log(`Not a valid URL (${e}):`, url);
return url;
}
return friendlyName && useLinkFriendlyNames ? `[url=${url}]${friendlyName}[/url]` : '[url]' + url + '[/url]';
}
function dcResolveLinks(wikiBody, replacer) {
if (typeof wikiBody != 'string' || typeof replacer != 'function') throw 'Invalid argument';
let lookupWorkers = [ ];
wikiBody = wikiBody.replace(/\[([armlu])=([^\[\]\r\n]+)\]/ig,
(match, key, id) => !/^\d+$/.test(id) ? replacer(key, id, dcNameNormalizer(id)) : match);
const entryExtractor = /\[([armlu])=?(\d+)\]/ig;
let match;
while ((match = entryExtractor.exec(wikiBody)) != null) {
const en1 = { key: match[1].toLowerCase(), id: parseInt(match[2]) };
if (!lookupWorkers.some(en2 => en2.key == en1.key && en2.id == en1.id)) lookupWorkers.push(en1);
}
lookupWorkers = lookupWorkers.map(entry => getDiscogsEntry(dcEntryTypes[entry.key], entry.id).then(result => ({
key: entry.key,
id: entry.id,
resolvedId: result.id,
name: (result.name ? dcNameNormalizer(result.name) : result.title).trim(),
})).catch(function(reason) {
alert(`Discogs lookup for ${match.key}${match.id} failed: ` + reason);
return null;
}));
return lookupWorkers.length > 0 ? Promise.all(lookupWorkers).then(function(entries) {
if ((entries = entries.filter(Boolean)).length > 0) return entries;
return Promise.reject('No entries were resolved');
}).then(entries => Object.assign.apply({ }, Object.keys(dcEntryTypes).map(key => ({ [key]: (function() {
const items = entries.filter(entry => entry.key == key).map(entry => ({ [entry.id]: entry.name }));
return items.length > 0 ? Object.assign.apply({ }, items) : { };
})() })))).then(lookupTable => wikiBody.replace(entryExtractor, function(match, key, id) {
const name = lookupTable[key = key.toLowerCase()][id = parseInt(id)];
if (!name) console.warn('Discogs item not resolved:', match);
return replacer(key, id, name);
})) : Promise.resolve(wikiBody);
}
function setProgressInfo(content, destructive = false, asHTML = false) {
function destroy() {
if (!(info instanceof HTMLElement)) return;
if (info.hTimer) clearTimeout(info.hTimer);
info.remove();
}
const id = 'discogs-progress-info';
let info = document.getElementById(id);
if (!content) return destroy();
if (info == null) {
info = document.createElement('DIV');
info.id = id;
info.style = 'margin-top: 1em;';
dcForm.append(info);
} else if (info.hTimer) clearTimeout(info.hTimer);
info[asHTML ? 'innerHTML' : 'textContent'] = content;
if (destructive) info.hTimer = setTimeout(destroy, 10000); else if (info.hTimer) delete info.hTimer;
}
function getDcArtistId() {
console.assert(dcInput instanceof HTMLInputElement, 'dcInput instanceof HTMLInputElement');
let m = /^(https?:\/\/(?:\w+\.)?discogs\.com\/artist\/(\d+))\b/i.exec(dcInput.value.trim());
if (m != null) return parseInt(m[2]);
console.warn('Discogs link isnot valid:', dcInput.value);
return (m = /^\/artist\/(\d+)\b/i.exec(dcInput.value)) != null ? parseInt(m[1]) : undefined;
}
function reliabilityColorValue(matched, total, colors = [0xccbf00, 0x008000]) {
if (!total) return;
console.assert(matched > 0, 'matched > 0');
if (matched <= 0) return '#' + colors[0].toString(16).padStart(6, '0');
if (matched >= total) return '#' + colors[1].toString(16).padStart(6, '0');
const colorIndexRate = Math.min(matched / total / 0.80, 1);
const colorsAsRGB = colors.map(color => [2, 1, 0].map(index => (color >> (index << 3)) & 0xFF));
const compositeValue = index => colorsAsRGB[0][index] +
Math.round(colorIndexRate * (colorsAsRGB[1][index] - colorsAsRGB[0][index]));
return `rgb(${compositeValue(0)}, ${compositeValue(1)}, ${compositeValue(2)})`;
}
function updateArtistWiki(bbCode, summary, editNotes, overwrite = 1) {
if (!bbCode || !(bbCode = bbCode.trim())) return false;
let elem = document.getElementById('body');
console.assert(elem != null, 'body != null');
if (elem == null || elem.value.includes(bbCode.replace(/\r?\n/g, '\n'))) return false;
if (elem.value.length <= 0 || overwrite > 1) {
if (isRED) bbCode = '[pad=6|0|0|0]' + bbCode + '[/pad]';
elem.value = bbCode;
} else if (overwrite <= 0) return false; else elem.value += '\n\n' + bbCode;
if (summary && (elem = document.body.querySelector('input[type="text"][name="summary"]')) != null)
elem.value = summary;
if (editNotes && (elem = document.getElementById('artisteditnotes')) != null
&& !elem.value.toLowerCase().includes(editNotes.toLowerCase()))
if (!elem.value) elem.value = editNotes; else elem.value += '\n\n' + editNotes;
return true
}
function genDcArtistDescriptionBB(artist) {
function link(key, id, title) {
if (!key || !id) throw 'Invalid argument';
const link = (title = key + id) =>
`[url=${encodeURI(`https://www.discogs.com/${dcEntryTypes[key]}/${id}`)}][plain]${title}[/plain][/url]`;
if (title) switch (key = key.toLowerCase()) {
case 'a': return `[artist]${dcNameNormalizer(title)}[/artist]${link('')}`;
// case 'l': return `[url=${document.location.origin}/torrents.php?${new URLSearchParams({
// action: 'advanced',
// remasterrecordlabel: dcNameNormalizer(title),
// }).toString()}]${dcNameNormalizer(title)}[/url]${link('')}`;
// case 'm': case 'r': return `[url=${document.location.origin}/torrents.php?${new URLSearchParams({
// action: 'advanced',
// groupname: dcNameNormalizer(title),
// }).toString()}]${dcNameNormalizer(title)}[/url]${link('')}`;
}
return link(title);
}
function addRelations(bbCode) {
const artistFormatter = (label, key) => `\n[b]${label}:[/b] ${artist[key].filter(artist => artist.active)
.concat(artist[key].filter(artist => isRED && !artist.active)).map(function(artist) {
const a = link('a', artist.id, artist.name);
return artist.active ? a : `[s]${a}[/s]`;
}).join(', ')}`;
if (members) bbCode += artistFormatter('Members', 'members');
if (groups) bbCode += artistFormatter('Member of'/*'In groups'*/, 'groups');
return bbCode.trim() || Promise.reject('no profile data');
}
if (!artist) throw 'The parameter is required';
const members = Array.isArray(artist.members) && artist.members.length > 0,
groups = Array.isArray(artist.groups) && artist.groups.length > 0;
let bbCode = !members && artist.realname && artist.realname != dcNameNormalizer(artist.name) ?
`[b]Real name:[/b] [plain]${artist.realname}[/plain]` : '';
if (artist.profile) {
const profile = artist.profile.trim()
.replace(/(?:[ \t]*\r?\n){2,}/g, '\n\n')
.replace(/\s*^(?:[Ff]or .+ (?:use|see|visit)|[Ss]ee also) \[a(?:=.+?|\d+)\].?$/gm, '')
.replace(/\[url=([^\[\]\r\n]+)\]([^\[\]\r\n]+)\[\/url\]/ig,
(m, url, title) => `[url=${url.trim()}]${title}[/url]`)
.replace(/\[url\]([^\[\]\r\n]+)\[\/url\]/ig, (m, url) => `[url]${url.trim()}[/url]`)
.replace(/\[img=([^\[\]\r\n]+)\]/ig, (m, url) => `[img]${url.trim()}[/img]`)
.replace(/\[t=?(\d+)\]/ig, '[url=https://www.discogs.com/help/forums/topic?topic_id=$1]topic $1[/url]')
.replace(/\[g=?([^\[\]\r\n]+)\]/ig, '[url=https://www.discogs.com/help/guidelines/$1]guideline $1[/url]');
return dcResolveLinks(profile, link).catch(reason => profile)
.then(profile => addRelations(bbCode + '\n\n' + profile + '\n'));
}
return Promise.resolve(bbCode).then(addRelations);
}
function genDcArtistTooltipHTML(artist, resolveIds = false) {
if (!artist) throw 'The parameter is required';
const linkColor = isLightTheme ? '#0A84AF' : isDarkTheme ? 'aqua' : 'cadetblue';
const link = (key, id, caption = key + id) =>
`${caption}`;
let html = `
$2') .replace(/\[quote=([^\[\]\r\n]+)\]([\S\s]+)\[\/quote\]/ig, '
$2') .replace(/\[g=?([^\[\]\r\n]+)\]/ig, 'guideline $1') .replace(/\[t=?(\d+)\]/ig, 'topic $1'); if (!resolveIds) profile = profile.replace(/\[([armlu])=?(\d+)\]/ig, (m, key, id) => link(key, id)); const tagConversions = { b: 'font-weight: bold;', i: 'font-style: italic;', u: 'text-decoration: underline;', s: 'text-decoration: line-through;', }; const BB2Html = str => '
' + Object.keys(tagConversions).reduce((str, key) =>
str.replace(new RegExp(`\\[${key}\\](.*?)\\[\\/${key}\\]`, 'ig'),
`$1`), str).replace(/(?:[ \t]*\r?\n)/g, '
') + '
' + status.body.slice(0, 2**10) + '
'; $(elems[5]).tooltipster({ content: tooltip.replace(/[ \t]*\r?\n/g, '