// ==UserScript==
// @name [GMT] Edition lookup by CD TOC
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.16.12
// @description Lookup edition by CD TOC on MusicBrainz, GnuDb and in CUETools DB, seed new/update existing MusicBrainz releases based on the TOC
// @match https://*/torrents.php?id=*
// @match https://*/torrents.php?page=*&id=*
// @run-at document-end
// @iconURL https://ptpimg.me/5t8kf8.png
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @connect musicbrainz.org
// @connect coverartarchive.org
// @connect archive.org
// @connect discogs.com
// @connect db.cuetools.net
// @connect db.cue.tools
// @connect gnudb.org
// @connect allmusic.com
// @connect accuraterip.com
// @connect api.translatedlabs.com
// @connect allmusic.com
// @author Anakunda
// @license GPL-3.0-or-later
// @resource mb_logo https://upload.wikimedia.org/wikipedia/commons/9/9e/MusicBrainz_Logo_%282016%29.svg
// @resource mb_icon https://upload.wikimedia.org/wikipedia/commons/9/9a/MusicBrainz_Logo_Icon_%282016%29.svg
// @resource mb_logo_text https://github.com/metabrainz/metabrainz-logos/raw/master/logos/MusicBrainz/SVG/MusicBrainz_logo.svg
// @resource mb_text https://github.com/metabrainz/metabrainz-logos/raw/master/logos/MusicBrainz/SVG/MusicBrainz_logo_text_only.svg
// @resource dc_icon https://upload.wikimedia.org/wikipedia/commons/6/69/Discogs_record_icon.svg
// @resource am_logo https://upload.wikimedia.org/wikipedia/commons/a/a0/AllMusic_Logo.svg
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
// @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
// @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// @require https://openuserjs.org/src/libs/Anakunda/libStringDistance.min.js
// ==/UserScript==
'use strict';
const sessionsCache = new Map;
let sessionsSessionCache, noEditPerms = document.getElementById('nav_userclass'), logScoresCache;
noEditPerms = noEditPerms != null && ['User', 'Member', 'Power User'].includes(noEditPerms.textContent.trim());
const [mbOrigin, dcOrigin] = ['https://musicbrainz.org', 'https://www.discogs.com'];
const toASCII = str => str && str.normalize('NFKD').replace(/[\x00-\x1F\u0300-\u036F]/gu, '');
const cmpNorm = str => str && toASCII(str).replace(/[\s\–\x00-\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F\u2019]+/g, '').toLowerCase();
const sameStringValues = (...strVals) => strVals.length > 0
&& strVals.every((strVal, ndx, arr) => strVal && cmpNorm(strVal) == cmpNorm(arr[0]));
const similarStringValues = (strVal1, strVal2, threshold = 0.95) => strVal1 && strVal2
&& (sameStringValues(strVal1, strVal2)
|| jaroWinklerSimilarity(toASCII(strVal1).toLowerCase(), toASCII(strVal2).toLowerCase()) >= threshold);
const createElements = (...tagNames) => tagNames.map(Document.prototype.createElement.bind(document));
const debugLogging = GM_getValue('debug_logging', false);
const mbID = /([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})/i.source;
const rxMBID = new RegExp(`^${mbID}$`, 'i');
const mbRequestsCache = new Map, mbRequestRate = 1000;
let mbLastRequest = null;
function mbApiRequest(endPoint, params) {
if (!endPoint) throw 'Endpoint is missing';
const url = new URL('/ws/2/' + endPoint.replace(/^\/+|\/+$/g, ''), mbOrigin);
if (params) for (let key in params) url.searchParams.set(key, params[key]);
url.searchParams.set('fmt', 'json');
const cacheKey = url.pathname.slice(6) + url.search;
if (mbRequestsCache.has(cacheKey)) return mbRequestsCache.get(cacheKey);
const request = new Promise(function(resolve, reject) {
let retryCounter = 0;
const xhr = {
method: 'GET', url: url, responseType: 'json', timeout: 60e3,
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: function(response) {
mbLastRequest = Date.now();
if (response.status >= 200 && response.status < 400) resolve(response.response);
else if (recoverableHttpErrors.includes(response.status) && ++retryCounter < 60) {
console.log('MusicBrainz API request retry #%d on HTTP error %d', retryCounter, response.status);
setTimeout(request, 1000);
} else reject(defaultErrorHandler(response));
},
onerror: response => { mbLastRequest = Date.now(); reject(defaultErrorHandler(response)); },
ontimeout: response => { mbLastRequest = Date.now(); reject(defaultTimeoutHandler(response)); },
}, request = () => {
if (mbLastRequest == Infinity) return setTimeout(request, 50);
const availableAt = mbLastRequest + mbRequestRate, now = Date.now();
if (now < availableAt) return setTimeout(request, availableAt - now); else mbLastRequest = Infinity;
GM_xmlhttpRequest(xhr);
};
request();
});
mbRequestsCache.set(cacheKey, request);
return request.catch(reason => (mbRequestsCache.delete(cacheKey), Promise.reject(reason)));
}
const dcApiRateControl = { }, dcApiRequestsCache = new Map;
const dcAuth = (function() {
const [token, consumerKey, consumerSecret] =
['discogs_api_token', 'discogs_api_consumerkey', 'discogs_api_consumersecret'].map(name => GM_getValue(name));
return token ? { token: token } : consumerKey && consumerSecret ?
{ key: consumerKey, secret: consumerSecret } : undefined;
})();
let dcApiResponses, quotaExceeded = false;
function dcApiRequest(endPoint, params) {
if (endPoint) endPoint = new URL(endPoint, 'https://api.discogs.com');
else return Promise.reject('No endpoint provided');
if (params instanceof URLSearchParams) endPoint.search = params;
else if (typeof params == 'object') for (let key in params) endPoint.searchParams.set(key, params[key]);
else if (params) endPoint.search = new URLSearchParams(params);
const cacheKey = endPoint.pathname.slice(1) + endPoint.search;
if (dcApiRequestsCache.has(cacheKey)) return dcApiRequestsCache.get(cacheKey);
if (!dcApiResponses && 'discogsApiResponseCache' in sessionStorage) try {
dcApiResponses = JSON.parse(sessionStorage.getItem('discogsApiResponseCache'));
} catch(e) {
sessionStorage.removeItem('discogsApiResponseCache');
console.warn(e);
}
if (dcApiResponses && cacheKey in dcApiResponses) return Promise.resolve(dcApiResponses[cacheKey]);
const reqHeader = { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' };
if (dcAuth) reqHeader.Authorization = 'Discogs ' +
Object.keys(dcAuth).map(key => key + '=' + dcAuth[key]).join(', ');
// if (dcAuth) for (let key in dcAuth) endPoint.searchParams.set(key, dcAuth[key]);
let requestsMax = dcAuth ? 60 : 25, retryCounter = 0;
const request = new Promise((resolve, reject) => (function request() {
const now = Date.now();
const postpone = (timeStamp = now) => { setTimeout(request, dcApiRateControl.timeFrameExpiry - timeStamp) };
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: reqHeader,
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) {
if (!quotaExceeded) try {
if (!dcApiResponses) dcApiResponses = { };
dcApiResponses[cacheKey] = response.response;
sessionStorage.setItem('discogsApiResponseCache', JSON.stringify(dcApiResponses));
} catch(e) {
quotaExceeded = true;
console.warn(e);
}
resolve(response.response);
} else if (response.status == 429/* && ++retryCounter < xhrLibmaxRetries*/) {
console.warn(defaultErrorHandler(response), response.response.message, '(' + retryCounter + ')',
`Rate limit used: ${requestsUsed}/${requestsMax}`);
postpone(Date.now());
} else if (recoverableHttpErrors.includes(response.status) && ++retryCounter < xhrLibmaxRetries)
setTimeout(request, xhrRetryTimeout);
else reject(defaultErrorHandler(response));
},
onerror: function(response) {
if (recoverableHttpErrors.includes(response.status) && ++retryCounter < xhrLibmaxRetries)
setTimeout(request, xhrRetryTimeout);
else reject(defaultErrorHandler(response));
},
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
}); else postpone();
})());
dcApiRequestsCache.set(cacheKey, request);
return request;
}
function mbIdExtractor(expr, entity) {
if (!expr || !expr) return null;
let mbId = rxMBID.exec(expr);
if (mbId) return mbId[1].toLowerCase(); else if (!entity) return null;
try { mbId = new URL(expr) } catch(e) { return null }
return mbId.hostname.endsWith('musicbrainz.org')
&& (mbId = new RegExp(`^\\/${entity}\\/${mbID}\\b`, 'i').exec(mbId.pathname)) != null ?
mbId[1].toLowerCase() : null;
}
let arOffsets;
function getAccuripOffsets() {
if (arOffsets instanceof Promise) return arOffsets;
const cachedOffsets = GM_getValue('read_offsets');
if (cachedOffsets && Object.keys(cachedOffsets).length <= 0) cachedOffsets = undefined;
if (cachedOffsets) {
const timeStamp = GM_getValue('read_offsets_time');
if (timeStamp > 0 && Date.now() - timeStamp < 24 * 60 * 60 * 1000)
return arOffsets = Promise.resolve(cachedOffsets);
}
return arOffsets = globalXHR('http://accuraterip.com/driveoffsets.htm').then(function({document}) {
const offsets = Object.assign.apply({ }, Array.from(document.body.querySelectorAll('table table > tbody > tr:not(:first-of-type)'), function(tr) {
const offset = {
driveId: tr.cells[0].textContent.trim().replace(/\s+/g, ' ').replace(/^\s*-\s*/, ''),
offset: parseInt(tr.cells[1].textContent),
submits: parseInt(tr.cells[2].textContent),
agreeRate: parseInt(tr.cells[3].textContent),
};
return offset.driveId && !isNaN(offset.offset) ? offset : null;
}).filter(Boolean).map(offset => ({ [offset.driveId]: [offset.offset, offset.submits, offset.agreeRate] })));
if (offsets.length <= 0) return Promise.reject('No drive offsets found');
GM_setValue('read_offsets_time', Date.now());
GM_setValue('read_offsets', offsets);
return offsets;
}).catch(reason => cachedOffsets || Promise.reject(reason));
}
const msf = 75, preGap = 2 * msf, msfTime = /(?:(\d+):)?(\d+):(\d+)[\.\:](\d+)/.source;
const msfToSector = time => Array.isArray(time) || (time = new RegExp('^\\s*' + msfTime + '\\s*$').exec(time)) != null ?
(((time[1] ? parseInt(time[1]) : 0) * 60 + parseInt(time[2])) * 60 + parseInt(time[3])) * msf + parseInt(time[4]) : NaN;
const rxRangeRip = /^(?:Selected range|Выбранный диапазон|Âûáðàííûé äèàïàçîí|已选择范围|選択された範囲|Gewählter Bereich|Intervallo selezionato|Geselecteerd bereik|Utvalt område|Seleccionar gama|Избран диапазон|Wybrany zakres|Izabrani opseg|Vybraný rozsah)(?:[^\S\r\n]+\((?:Sectors|Секторы|扇区|Sektoren|Settori|Sektorer|Sectores|Сектори|Sektora|Sektory)[^\S\r\n]+(\d+)[^\S\r\n]*-[^\S\r\n]*(\d+)\))?$/m;
const sessionHeader = '(?:' + [
'(?:EAC|XLD) (?:extraction logfile from |Auslese-Logdatei vom |extraheringsloggfil från |uitlezen log bestand van |log súbor extrakcie z |抓取日志文件从)',
'File di log (?:EAC|XLD) per l\'estrazione del ', 'Archivo Log de extracciones desde ',
'Отчёт (?:EAC|XLD) об извлечении, выполненном ', 'Отчет на (?:EAC|XLD) за извличане, извършено на ',
'Protokol extrakce (?:EAC|XLD) z ', 'Sprawozdanie ze zgrywania programem (?:EAC|XLD) z ',
'(?:EAC|XLD)-ov fajl dnevnika ekstrakcije iz ',
'(?:Log created by: whipper|EZ CD Audio Converter) .+(?:\\r?\\n)+Log creation date: ',
'morituri extraction logfile from ', 'Rip .+ Audio Extraction Log',
].join('|') + ')';
const rxTrackExtractor = /^(?:(?:(?:Track|Трек|Òðåê|音轨|Traccia|Spår|Pista|Трак|Utwór|Stopa)[^\S\r\n]+\d+\b.*|Track \d+ saved to\b.+)$(?:\r?\n^(?:[^\S\r\n]+.*)?$)*| +\d+:$\r?\n^ {4,}Filename:.+$(?:\r?\n^(?: {4,}.*)?$)*)/gm;
function getTocEntries(session) {
if (!session) return null;
const tocParsers = [
'^\\s+' + ['\\d+', msfTime, msfTime, '\\d+', '\\d+'] // EAC / XLD
.map(pattern => '(' + pattern + ')').join('\\s+\\|\\s+') + '\\s*$',
'^\\s+\[X\]\\s+' + ['\\d+', msfTime, msfTime, '\\d+', '\\d+'] // EZ CD
.map(pattern => '(' + pattern + ')').join('\\s+') + '\\b',
// whipper
'^ +(\\d+): *' + [['Start', msfTime], ['Length', msfTime], ['Start sector', '\\d+'], ['End sector', '\\d+']]
.map(([label, capture]) => `\\r?\\n {4,}${label}: *(${capture})\\b *`).join(''),
// Rip
'^\\s+' + ['(\\d+)', '(' + msfTime + ')', '(?:(?:\\d+):)?\\d+:\\d+[\\.\\:]\\d+', '(' + msfTime + ')', '(\\d+)', '(\\d+)', '\\d+']
.join('\\s+\\|\\s+') + '\\s*$',
];
let tocEntries = tocParsers.reduce((m, rx) => m || session.match(new RegExp(rx, 'gm')), null);
if (tocEntries == null || (tocEntries = tocEntries.map(function(tocEntry, trackNdx) {
if ((tocEntry = tocParsers.reduce((m, rx) => m || new RegExp(rx).exec(tocEntry), null)) == null)
throw `assertion failed: track ${trackNdx + 1} ToC entry invalid format`;
const [startSector, endSector] = [12, 13].map(index => parseInt(tocEntry[index]));
console.assert(msfToSector(tocEntry[2]) == startSector && msfToSector(tocEntry[7]) == endSector + 1 - startSector
&& endSector >= startSector, 'TOC table entry validation failed:', tocEntry);
return { trackNumber: parseInt(tocEntry[1]), startSector: startSector, endSector: endSector };
})).length <= 0) return null;
if (!tocEntries.every((tocEntry, trackNdx) => tocEntry.trackNumber == trackNdx + 1)) {
tocEntries = Object.assign.apply({ }, tocEntries.map(tocEntry => ({ [tocEntry.trackNumber]: tocEntry })));
tocEntries = Object.keys(tocEntries).sort((a, b) => parseInt(a) - parseInt(b)).map(key => tocEntries[key]);
}
console.assert(tocEntries.every((tocEntry, trackNdx, tocEntries) => tocEntry.trackNumber == trackNdx + 1
&& tocEntry.endSector >= tocEntry.startSector && (trackNdx <= 0 || tocEntry.startSector > tocEntries[trackNdx - 1].endSector)),
'TOC table structure validation failed:', tocEntries);
return tocEntries;
}
function getTrackDetails(session) {
function extractValues(patterns, ...callbacks) {
if (!Array.isArray(patterns) || patterns.length <= 0) return null;
const rxs = patterns.map(pattern => new RegExp('^[^\\S\\r\\n]+' + pattern + '\\s*$', 'm'));
return trackRecords.map(function(trackRecord, trackNdx) {
trackRecord = rxs.map(rx => rx.exec(trackRecord));
const index = trackRecord.findIndex(matches => matches != null);
return index < 0 || typeof callbacks[index] != 'function' ? null : callbacks[index](trackRecord[index]);
});
}
if (rxRangeRip.test(session)) return { }; // Nothing to extract from RR
const trackRecords = session.match(rxTrackExtractor);
if (trackRecords == null) return { };
const h2i = m => parseInt(m[1], 16);
return Object.assign({ crc32: extractValues([
'(?:(?:Copy|复制|Kopie|Copia|Kopiera|Copiar|Копиран) CRC|CRC (?:копии|êîïèè|kopii|kopije|kopie|kópie)|コピーCRC)\\s+([\\da-fA-F]{8})', // 1272
'(?:CRC32 hash|Copy CRC|CRC)\\s*:\\s*([\\da-fA-F]{8})',
], h2i, h2i), peak: extractValues([
'(?:Peak level|Пиковый уровень|Ïèêîâûé óðîâåíü|峰值电平|ピークレベル|Spitzenpegel|Pauze lengte|Livello di picco|Peak-nivå|Nivel Pico|Пиково ниво|Poziom wysterowania|Vršni nivo|[Šš]pičková úroveň)\\s+(\\d+(?:\\.\\d+)?)\\s*\\%', // 1217
'(?:Peak(?: level)?)\\s*:\\s*(\\d+(?:\\.\\d+)?)',
], m => [parseFloat(m[1]) * 10, 3], m => [parseFloat(m[1]) * 1000, 6]), preGap: extractValues([
'(?:Pre-gap length|Длина предзазора|Äëèíà ïðåäçàçîðà|前间隙长度|Pausenlänge|Durata Pre-Gap|För-gap längd|Longitud Pre-gap|Дължина на предпразнина|Długość przerwy|Pre-gap dužina|[Dd]élka mezery|Dĺžka medzery pred stopou)\\s+' + msfTime, // 1270
'(?:Pre-gap length)\\s*:\\s*' + msfTime,
], msfToSector, msfToSector) }, Object.assign.apply(undefined, [1, 2].map(v => ({ ['arv' + v]: extractValues([
'.+?\\[([\\da-fA-F]{8})\\].+\\(AR v' + v + '\\)',
'(?:AccurateRip v' + v + ' signature)\\s*:\\s*([\\da-fA-F]{8})',
], h2i, h2i) }))));
}
function getUniqueSessions(logFiles, detectVolumes = GM_getValue('detect_volumes', false)) {
logFiles = Array.prototype.map.call(logFiles, function(logFile) {
while (logFile.startsWith('\uFEFF')) logFile = logFile.slice(1);
return logFile;
});
const rxRipperSignatures = '(?:(?:' + [
'Exact Audio Copy V', 'X Lossless Decoder version ', 'CUERipper v',
'EZ CD Audio Converter ', 'Log created by: whipper ', 'morituri version ', 'Rip ',
].join('|') + ')\\d+)';
const verifyOffsets = sessions => sessions.length > 0 ? Promise.all(sessions.map(function(session) {
let usedDrive = [
/^(?:Used drive|Benutztes Laufwerk|Unità predefinita|Usar unidad|Използвано устройство|使用驱动器|Použitá mechanika|Använd enhet|gebruikt loopwerk|Użyty napęd|Дисковод|Korišćen drajv|Äèñêîâîä|Drive used)\s*:\s*(.+)$/im,
/^(?:Device: *\(?:\[ ?[A-Z]: ?\] *)? \s*:\s*(.+)$/m, // EZCD
].reduce((matches, rx) => matches || rx.exec(session), null);
let readOffset = /^(?:Read offset(?: correction)?|Коррекция смещения при чтении|读取偏移校正|Leseoffset Korrektur|Correzione offset di lettura|Corrección de Desplazamiento de Lectura|Lees-offset correctie|Läs-offset-korrigering|Офсет корекция при четене|Korekta położenia dla odczytu|Korekce vychýlení čtení|Offsetová korekcia pre čítanie|Korekcija offset-a kod čitanja|Êîððåêöèÿ ñìåùåíèÿ ïðè ÷òåíèè|Sample offset)\s*:\s*(\d+)\b/im.exec(session);
if (usedDrive == null || readOffset == null) return Promise.resolve(undefined);
usedDrive = usedDrive[1].replace(/\s+(?:(?:Adapter|ID):\s+\d+.*|\((?:not found|revision)\b.+\))$/, '').replace(/\s+/g, ' ').trim();
readOffset = parseInt(readOffset[1]);
return getAccuripOffsets().then(function(arOffsets) {
const drives = Object.keys(arOffsets).filter(key => sameStringValues(key, usedDrive) || sameStringValues(key, [
[/^(?:JLMS)\b/, 'Lite-ON'],
[/^(?:HL-DT-ST|HL[ -]?DT[ -]?ST\b)/, 'LG Electronics'],
[/^(?:Matshita)\b/i, 'Panasonic'],
].reduce((driveStr, subst) => driveStr.replace(...subst), usedDrive)));
if (drives.length <= 0) return undefined;
console.info('Read offset(s) for %s found in AR database:', usedDrive, drives.map(drive =>
`${arOffsets[drive][0] > 0 ? '+' + arOffsets[drive][0] : arOffsets[drive][0]} (submits: ${arOffsets[drive][1]}, agree rate: ${arOffsets[drive][2]})`));
const matches = drives.map(function(drive) {
if (arOffsets[drive][1] >= 5 || arOffsets[drive][2] >= 100) return readOffset == arOffsets[drive][0];
console.info('Weak read offset for', drive, 'found in AR database - offset not verified');
});
if (matches.some(Boolean)) return true; else if (matches.some(match => match != false)) return undefined;
console.warn('Mismatching read offset for', usedDrive, matches);
return false;
}, reason => { console.warn(reason) });
})).then(results => !results.some(result => result == false)
|| confirm('At least one disc was ripped with incorrect read offset, continue anyway?')
? sessions : Promise.reject('Incorrect read offset')) : null;
if (!detectVolumes) {
const rxStackedLog = new RegExp('^[\\S\\s]*(?:\\r?\\n)+(?=' + rxRipperSignatures + ')');
logFiles = logFiles.map(logFile => rxStackedLog.test(logFile) ? logFile.replace(rxStackedLog, '') : logFile)
.filter(RegExp.prototype.test.bind(new RegExp('^(?:' + rxRipperSignatures + '|' + sessionHeader + ')')));
return verifyOffsets(logFiles);
}
if ((logFiles = logFiles.map(function(logFile) {
let rxSessionsIndexer = new RegExp('^' + rxRipperSignatures, 'gm'), indexes = [ ], match;
while ((match = rxSessionsIndexer.exec(logFile)) != null) indexes.push(match.index);
if (indexes.length <= 0) {
rxSessionsIndexer = new RegExp('^' + sessionHeader, 'gm');
while ((match = rxSessionsIndexer.exec(logFile)) != null) indexes.push(match.index);
}
return (indexes = indexes.map((index, ndx, arr) => logFile.slice(index, arr[ndx + 1])).filter(function(logFile) {
const rr = rxRangeRip.exec(logFile);
if (rr == null) return true;
// Ditch HTOA logs
const tocEntries = getTocEntries(logFile);
return tocEntries == null || parseInt(rr[1]) != 0 || parseInt(rr[2]) + 1 != tocEntries[0].startSector;
})).length > 0 ? indexes : null;
}).filter(Boolean)).length <= 0) return null;
const sessions = new Map;
for (const logFile of logFiles) for (const session of logFile) {
let uniqueKey = getTocEntries(session), title;
if (uniqueKey != null) uniqueKey = [uniqueKey[0].startSector].concat(uniqueKey.map(tocEntry =>
tocEntry.endSector + 1)).map(offset => offset.toString(32).padStart(4, '0')).join(''); else continue;
if ((title = new RegExp('^' + sessionHeader + '(.+)$(?:\\r?\\n)+^(.+ [\\/\\-] .+)$', 'm').exec(session)) != null)
title = title[2];
else if ((title = /^ +Release: *$\r?\n^ +Artist: *(.+)$\r?\n^ +Title: *(.+)$/m.exec(session)) != null)
title = title[1] + ' / ' + title[2]; // Whipper?
else if ((title = /^Compact Disc Information\r?\n=+\r?\nName: *(.+)$/m.exec(session)) != null)
title = title[1]; // Rip
if (title != null) uniqueKey += '/' + title.replace(/\s+/g, '').toLowerCase();
sessions.set(uniqueKey, session);
}
//console.info('Unique keys:', Array.from(sessions.keys()));
return sessions.size > 0 ? verifyOffsets(Array.from(sessions.values())) : null;
}
function getSessions(torrentId) {
if (!(torrentId > 0)) throw 'Invalid argument';
if (sessionsCache.has(torrentId)) return sessionsCache.get(torrentId);
if (!sessionsSessionCache && 'ripSessionsCache' in sessionStorage) try {
sessionsSessionCache = JSON.parse(sessionStorage.getItem('ripSessionsCache'));
} catch(e) {
console.warn(e);
sessionStorage.removeItem('ripSessionsCache');
sessionsSessionCache = undefined;
}
if (!logScoresCache && (logScoresCache = sessionStorage.getItem('logScoresCache'))) try {
logScoresCache = JSON.parse(logScoresCache);
} catch(e) {
console.warn(e);
sessionStorage.removeItem('logScoresCache');
logScoresCache = undefined;
}
if (sessionsSessionCache && torrentId in sessionsSessionCache)
return Promise.resolve(sessionsSessionCache[torrentId]);
// let request = queryAjaxAPICached('torrent', { id: torrentId });
// request = request.then(({torrent}) => torrent.logCount > 0 ? Promise.all(torrent.ripLogIds.map(ripLogId =>
// queryAjaxAPICached('riplog', { id: torrentId, logid: ripLogId }).then(response => response))) : Promise.reject('No logfiles attached'));
let request = localXHR('/torrents.php?' + new URLSearchParams({ action: 'loglist', torrentid: torrentId }));
request.then(function(document) {
const spans = Array.from(document.body.querySelectorAll(':scope > blockquote > strong + span'), function(span) {
const result = { score: parseInt(span.textContent) };
console.assert(!isNaN(result.score), span.cloneNode());
if ((span = span.parentNode.nextElementSibling) != null
&& (span = span.querySelector(':scope > h3 + pre')) != null
&& (span = span.textContent.split(/\r?\n/).map(deduction => deduction.trim()).filter(Boolean)).length > 0)
result.deductions = span;
return result;
});
if (spans.length <= 0) return;
if (!logScoresCache) logScoresCache = { };
logScoresCache[torrentId] = spans;
sessionStorage.setItem('logScoresCache', JSON.stringify(logScoresCache));
});
request = request.then(document => Array.from(document.body.querySelectorAll(':scope > blockquote > pre:first-child'),
pre => pre.textContent));
sessionsCache.set(torrentId, (request = request.then(getUniqueSessions).then(function(sessions) {
if (sessions == null) return Promise.reject('No valid logfiles attached'); else if (!quotaExceeded) try {
if (!sessionsSessionCache) sessionsSessionCache = { };
sessionsSessionCache[torrentId] = sessions;
sessionStorage.setItem('ripSessionsCache', JSON.stringify(sessionsSessionCache));
} catch(e) {
quotaExceeded = true;
console.warn(e);
}
return sessions;
})));
return request;
}
function getLayoutType(tocEntries) {
for (let index = 0; index < tocEntries.length - 1; ++index) {
const gap = tocEntries[index + 1].startSector - tocEntries[index].endSector - 1;
if (gap != 0) return (gap == 11400 ? tocEntries.length - index : 0) - 1;
}
return 0;
}
function lookupByToc(torrentId, callback) {
if (typeof callback != 'function') return Promise.reject('Invalid argument');
return getSessions(torrentId).then(sessions => Promise.all(sessions.map(function(session, volumeNdx) {
const isRangeRip = rxRangeRip.test(session), tocEntries = getTocEntries(session);
if (tocEntries == null) throw `disc ${volumeNdx + 1} ToC not found`;
let layoutType = getLayoutType(tocEntries);
if (layoutType < 0) console.warn('Disc %d unknown layout type', volumeNdx + 1);
else while (layoutType-- > 0) tocEntries.pop(); // ditch data tracks for CD with data track(s)
return callback(tocEntries, volumeNdx, sessions.length);
}).map(results => results.catch(function(reason) {
console.log('Edition lookup failed for the reason:', reason);
return Promise.reject(reason);
}))));
}
class DiscID {
#data = '';
addData(values, width = 0, length = 0) {
if (!values) return this; else if (!Array.isArray(values)) values = [values];
values = values.map(value => value.toString(16).toUpperCase().padStart(width, '0')).join('');
this.#data += width > 0 && length > 0 ? values.padEnd(length * width, '0') : values;
return this;
}
get digest() {
return CryptoJS.SHA1(this.#data).toString(CryptoJS.enc.Base64)
.replace(/\=/g, '-').replace(/\+/g, '.').replace(/\//g, '_');
}
}
function mbComputeDiscID(mbTOC) {
if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98)
throw 'Invalid or too long MB TOC';
return new DiscID().addData(mbTOC.slice(0, 2), 2).addData(mbTOC.slice(2), 8, 100).digest;
}
function tocEntriesToMbTOC(tocEntries) {
if (!Array.isArray(tocEntries) || tocEntries.length <= 0) throw 'Invalid argument';
const isHTOA = tocEntries[0].startSector > preGap, mbTOC = [tocEntries[0].trackNumber, tocEntries.length];
mbTOC.push(preGap + tocEntries[tocEntries.length - 1].endSector + 1);
return Array.prototype.concat.apply(mbTOC, tocEntries.map(tocEntry => preGap + tocEntry.startSector));
}
if (typeof unsafeWindow == 'object') {
unsafeWindow.lookupByToc = lookupByToc;
unsafeWindow.mbComputeDiscID = mbComputeDiscID;
unsafeWindow.tocEntriesToMbTOC = tocEntriesToMbTOC;
}
function getCDDBiD(tocEntries) {
if (!Array.isArray(tocEntries)) throw 'Invalid argument';
const tt = Math.floor((tocEntries[tocEntries.length - 1].endSector + 1 - tocEntries[0].startSector) / msf);
let discId = tocEntries.reduce(function(sum, tocEntry) {
let n = Math.floor((parseInt(tocEntry.startSector) + preGap) / msf), s = 0;
while (n > 0) { s += n % 10; n = Math.floor(n / 10) }
return sum + s;
}, 0) % 0xFF << 24 | tt << 8 | tocEntries.length;
if (discId < 0) discId = 2**32 + discId;
return discId.toString(16).toLowerCase().padStart(8, '0');
}
function getARiD(tocEntries) {
if (!Array.isArray(tocEntries)) throw 'Invalid argument';
const discIds = [0, 0];
for (let index = 0; index < tocEntries.length; ++index) {
discIds[0] += tocEntries[index].startSector;
discIds[1] += Math.max(tocEntries[index].startSector, 1) * (index + 1);
}
discIds[0] += tocEntries[tocEntries.length - 1].endSector + 1;
discIds[1] += (tocEntries[tocEntries.length - 1].endSector + 1) * (tocEntries.length + 1);
return discIds.map(discId => discId.toString(16).toLowerCase().padStart(8, '0'))
.concat(getCDDBiD(tocEntries)).join('-');
}
const noLabel = 'self-released';
const [rxNoLabel, rxBareLabel, rxNoCatno, rxCatNoRange] = [
/^(?:(?:No(?:t\s+On)?\s+Label|None|iMD|Independ[ae]nt)\b|\[(?:no\s+label|none)\]$)|\b(?:Self[\-\s]?Released?)\b/i,
[/^(?:The)\s+|(?:\s+\b(?:Record(?:ings|s)?|(?:Production|Publishing)s?|Corporation|Limited|Int(?:ernationa|\')?l|Discos)\b|,?\s+(?:Ltd|Inc|Co(?:rp)?|LLC|Intl)\.?)+$/ig, ''],
/^(?:None|\[none\])$/i, /^(.*?)(\d+)~(\d+)$/,
];
function setTooltip(elem, tooltip, params) {
if (!(elem instanceof HTMLElement)) throw 'Invalid argument';
if (typeof jQuery.fn.tooltipster == 'function') {
if (tooltip) tooltip = tooltip.replace(/\r?\n/g, '<br>')
if ($(elem).data('plugin_tooltipster'))
if (tooltip) $(elem).tooltipster('update', tooltip).tooltipster('enable');
else $(elem).tooltipster('disable');
else if (tooltip) $(elem).tooltipster(Object.assign(params || { }, { content: tooltip }));
} else if (tooltip) elem.title = tooltip; else elem.removeAttribute('title');
}
function getTorrentId(tr) {
if (!(tr instanceof HTMLElement)) throw 'Invalid argument';
if ((tr = tr.querySelector('a.button_pl')) != null
&& (tr = parseInt(new URLSearchParams(tr.search).get('torrentid'))) > 0) return tr;
}
function updateEdition(evt) {
if (noEditPerms || /*!openTabHandler(evt) || */evt.currentTarget.disabled) return false; else if (!ajaxApiKey) {
if (!(ajaxApiKey = prompt('Set your API key with torrent edit permission:\n\n'))) return false;
GM_setValue('redacted_api_key', ajaxApiKey);
}
const target = evt.currentTarget, payload = { }, torrentDetails = target.closest('tr.torrentdetails');
if (torrentDetails == null || !('editionGroup' in torrentDetails.dataset)) return false;
if (target.dataset.remasterYear) payload.remaster_year = target.dataset.remasterYear; else return false;
if (target.dataset.remasterRecordLabel) payload.remaster_record_label = target.dataset.remasterRecordLabel;
if (target.dataset.remasterCatalogueNumber) payload.remaster_catalogue_number = target.dataset.remasterCatalogueNumber;
if (target.dataset.remasterTitle) payload.remaster_title = target.dataset.remasterTitle.slice(0, 80);
if (Object.keys(payload).length <= 0) return false; else if (Boolean(eval(target.dataset.confirm))) {
const entries = [ ];
if ('remaster_year' in payload) entries.push('Edition year: ' + payload.remaster_year);
if ('remaster_record_label' in payload) entries.push('Record label: ' + payload.remaster_record_label);
if ('remaster_catalogue_number' in payload) entries.push('Catalogue number: ' + payload.remaster_catalogue_number);
if ('remaster_title' in payload) entries.push('Edition title: ' + payload.remaster_title);
if (!confirm('Edition group is going to be updated\n\n' + entries.join('\n') +
'\n\nAre you sure the information is correct?')) return false;
}
[target.disabled, target.style.color] = [true, 'orange'];
Promise.all(Array.from(document.body.querySelectorAll('table#torrent_details > tbody > tr.torrent_row.edition_' + torrentDetails.dataset.editionGroup), function(tr) {
const torrentId = getTorrentId(tr);
if (!(torrentId > 0)) return null;
const postData = new URLSearchParams(payload);
if (target.dataset.releaseUrl && torrentId == parseInt(torrentDetails.dataset.torrentId)
&& !(torrentDetails.dataset.torrentDescription || '').toLowerCase().includes(target.dataset.releaseUrl.toLowerCase()))
postData.set('release_desc', ((torrentDetails.dataset.torrentDescription || '') + '\n\n').trimLeft() +
'[url]' + target.dataset.releaseUrl + '[/url]');
return queryAjaxAPI('torrentedit', { id: torrentId }, postData);
})).then(function(responses) {
target.style.color = '#0a0';
console.log('Edition updated successfully:', responses);
document.location.reload();
}, function(reason) {
target.style.color = 'red';
alert(reason);
target.disabled = false;
});
return false;
}
function applyOnClick(tr) {
[tr.style.cursor, tr.dataset.confirm, tr.onclick] = ['pointer', true, updateEdition];
setTooltip(tr, 'Use simple click to apply edition info from this release', { position: 'right' });
tr.onmouseenter = tr.onmouseleave = evt => { evt.currentTarget.style.backgroundColor =
evt.type == 'mouseenter' ? '#FFA50040' : evt.currentTarget.dataset.backgroundColor || null };
}
function applyOnCtrlClick(tr) {
function updateStyle(state) {
tr.style.cursor = state ? 'pointer' : 'auto';
tr.style.backgroundColor = state ? '#FFA50040' : tr.dataset.backgroundColor || null;
}
tr.dataset.confirm = true;
const eventHandler = evt => { updateStyle(evt.ctrlKey) }, events = ['keyup', 'keydown'];
const listenerChanger = name => { for (let evt of events) document[name + 'EventListener'](evt, eventHandler) };
tr.onclick = evt => evt.ctrlKey ? updateEdition(evt) : true;
tr.onmouseenter = evt => { updateStyle(evt.ctrlKey); listenerChanger('add') };
tr.onmouseleave = evt => { updateStyle(false); listenerChanger('remove') };
setTooltip(tr, 'Use Ctrl to apply edition info from this release', { position: 'right' });
}
function addLookupResults(torrentId, ...elems) {
if (!(torrentId > 0)) throw 'Invalid argument'; else if (elems.length <= 0) return;
let elem = document.getElementById('torrent_' + torrentId);
if (elem == null) throw '#torrent_' + torrentId + ' not found';
let container = elem.querySelector('div.toc-lookup-tables');
if (container == null) {
if ((elem = elem.querySelector('div.linkbox')) == null) throw 'linkbox not found';
elem.after(container = Object.assign(document.createElement('div'), {
className: 'toc-lookup-tables',
style: 'margin: 10pt 0; padding: 0; display: flex; flex-flow: column; row-gap: 10pt;',
}));
}
(elem = document.createElement('div')).append(...elems);
container.append(elem);
}
function decodeHTML(html) {
const textArea = document.createElement('textarea');
textArea.innerHTML = html;
return textArea.value;
}
const promptEx = (title, prompt, required, input, ...options) => new Promise(function(resolve) {
const dialog = Object.assign(document.createElement('dialog'), {
style: 'margin: auto; padding: 15pt; color: white; background-color: #444; border: 2pt solid #222; box-shadow: 0 0 10pt black;',
onclose: function(evt) {
evt.currentTarget.remove();
resolve(evt.currentTarget.returnValue == 'OK' ? Object.assign({ input: releaseId.value }, options) : null);
},
}), form = Object.assign(document.createElement('form'), {
method: 'dialog',
style: 'display: flex; flex-flow: column; row-gap: 7pt; width: 35em; font: 9pt "Noto Sans", sans-serif;',
onsubmit: function(evt) {
options = [ ];
for (let input of evt.currentTarget.querySelectorAll('input[type="checkbox"]')) {
const groupNdx = parseInt(input.closest('fieldset').dataset.index), index = parseInt(input.dataset.index);
console.assert(groupNdx >= 0 && index >= 0);
if (!(groupNdx in options)) options[groupNdx] = [ ];
options[groupNdx][index] = input.checked;
}
},
}), releaseId = Object.assign(document.createElement('input'), {
type: 'text',
style: 'display: block; width: 100%; box-sizing: border-box; font: 10pt "Noto Sans", sans-serif;',
value: input || '', required: required, disabled: Boolean(input),
autocomplete: 'off',
oninput: evt => { if (optionsGroup != null) optionsGroup.disabled = !evt.currentTarget.value },
selectionStart: 0, selectionDirection: 'backward',
});
form.append(Object.assign(document.createElement('div'), {
textContent: title,
style: 'margin-bottom: 5pt; font-weight: bold; font-size: medium; color: coral;',
}));
if (prompt) {
const label = Object.assign(document.createElement('label'), {
textContent: prompt,
style: 'white-space: pre-line;',
});
releaseId.style.marginTop = '4pt';
label.append(releaseId);
form.append(label);
} else form.append(releaseId);
options.forEach(function(optionsGroup, groupIndex) {
const fieldset = Object.assign(document.createElement('fieldset'), {
style: 'box-sizing: border-box; padding: 5pt; display: flex; flex-flow: column; row-gap: 4pt; border: 4pt groove #555;',
className: 'prompt-options-' + (groupIndex + 1),
disabled: groupIndex == 0 && !Boolean(input),
});
fieldset.dataset.index = groupIndex;
optionsGroup.forEach(function(option, index) {
if (!option[0]) return;
const label = Object.assign(document.createElement('label'), {
style: 'display: block;',
}), checkbox = Object.assign(document.createElement('input'), {
type: 'checkbox',
checked: Boolean(option[1]),
style: 'margin-right: 5pt;',
});
if (option[2]) label.title = option[2];
checkbox.dataset.index = index;
if (option[1] === null) [label, checkbox].forEach(elem => { elem.disabled = true });
label.append(checkbox, option[0]);
fieldset.append(label);
});
if (fieldset.childElementCount > 0) form.append(fieldset);
});
const optionsGroup = form.querySelector('fieldset.prompt-options-1'), buttons = document.createElement('div');
buttons.style = 'display: flex; flex-flow: row; justify-content: flex-end; column-gap: 5pt; margin-top: 5pt;';
const buttonStyle = 'flex-basis: 5em; cursor: pointer; font: 9pt "Noto Sans", sans-serif;';
buttons.append(Object.assign(document.createElement('input'), {
type: 'submit',
value: 'OK',
style: buttonStyle,
}), Object.assign(document.createElement('input'), {
type: 'button',
value: 'Cancel',
style: buttonStyle,
onclick: evt => { dialog.close() },
}));
form.append(buttons);
dialog.append(form);
document.body.append(dialog);
dialog.showModal();
});
const editableHosts = GM_getValue('editable_hosts', ['redacted.ch']);
const incompleteEdition = /^(?:\d+ -|(?:Unconfirmed Release(?: \/.+)?|Unknown Release\(s\)) \/) CD$/;
const minifyHTML = html => html.replace(/\s*(?:\r?\n)+\s*/g, '');
const svgFail = (color = 'red', height = '0.9em') => minifyHTML(`
<svg height="${height}" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle fill="${color}" cx="50" cy="50" r="50"/>
<polygon fill="white" clip-path="circle(35)" points="19.95,90.66 9.34,80.05 39.39,50 9.34,19.95 19.95,9.34 50,39.39 80.05,9.34 90.66,19.95 60.61,50 90.66,80.05 80.05,90.66 50,60.61"/>
</svg>`);
const svgCheckmark = (color = '#0c0', height = '0.9em') => minifyHTML(`
<svg height="${height}" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle fill="${color}" cx="50" cy="50" r="50"/>
<path fill="white" d="M70.78 21.13c-2.05,0.44 -2.95,2.61 -3.98,4.19 -1.06,1.6 -2.28,3.22 -3.38,4.82 -6.68,9.75 -13.24,19.58 -19.9,29.34 -1.47,2.16 -1.1,1.99 -1.8,1.24 -1.95,-2.07 -4.14,-3.99 -6.18,-6.1 -1.36,-1.4 -2.72,-2.66 -4.06,-4.11 -1.44,-1.54 -3.14,-2.77 -5.29,-1.72 -1.18,0.57 -3.2,2.92 -4.35,3.98 -4.54,4.2 0.46,6.96 2.89,9.64 1.29,1.43 2.71,2.78 4.08,4.14 2.75,2.73 5.42,5.46 8.24,8.12 1.4,1.33 2.66,3.09 4.46,3.84 2.15,0.9 4.38,0.42 5.87,-1.39 1.03,-1.24 2.32,-3.43 3.31,-4.86 8.93,-12.94 17.53,-26.19 26.5,-39.06 1.1,-1.59 2.82,-3.29 2.81,-5.35 -0.02,-2.35 -2.03,-3.22 -3.69,-4.36 -1.69,-1.16 -3.25,-2.84 -5.53,-2.36z"/>
</svg>`);
const svgQuestionMark = (color = '#fc0', height = '0.9em') => minifyHTML(`
<svg height="${height}" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle fill="${color}" cx="50" cy="50" r="50"/>
<path fill="white" fill-rule="nonzero" d="M74.57 31.75c0,7.23 -3.5,13.58 -10.5,19.08 -2.38,1.9 -4.05,3.55 -5.03,4.99 -0.99,1.43 -1.47,3.21 -1.47,5.33l0 1.7 -16.4 0 0 -4.3c0,-5.97 1.97,-10.63 5.92,-14.02 2.56,-2.18 4.21,-3.68 4.94,-4.5 0.74,-0.81 1.27,-1.6 1.57,-2.35 0.32,-0.75 0.47,-1.63 0.47,-2.63 0,-1.23 -0.55,-2.28 -1.63,-3.13 -1.11,-0.85 -2.51,-1.27 -4.24,-1.27 -5.25,0 -10.76,2.1 -16.53,6.3l0 -19.17c2.12,-1.2 4.98,-2.23 8.58,-3.11 3.6,-0.89 6.82,-1.32 9.68,-1.32 7.99,0 14.09,1.57 18.3,4.72 4.22,3.13 6.34,7.7 6.34,13.68zm-13 45c0,2.9 -1.05,5.27 -3.15,7.12 -2.1,1.85 -4.92,2.78 -8.47,2.78 -3.43,0 -6.21,-0.95 -8.36,-2.85 -2.15,-1.9 -3.22,-4.25 -3.22,-7.05 0,-2.85 1.05,-5.17 3.15,-6.95 2.1,-1.77 4.92,-2.65 8.43,-2.65 3.48,0 6.28,0.88 8.42,2.65 2.13,1.78 3.2,4.1 3.2,6.95z"/>
</svg>
`);
const svgAniSpinner = (color = 'orange', phases = 12, height = '0.9em') => minifyHTML(`
<svg fill="${color}" style="scale: 2.5;" height="${height}" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
${Array.from(Array(phases)).map((_, ndx) => `
<rect x="47.5" y="27" rx="2.5" ry="4.16" width="5" height="16" transform="rotate(${Math.round((phases - ndx) * 360 / phases)} 50 50)">
<animate attributeName="opacity" values="1; 0;" keyTimes="0; 1;" dur="1s" begin="${Math.round(-1000 * ndx / phases)}ms" repeatCount="indefinite"></animate>
</rect>`).join('')}</svg>`);
const staticIconColor = 'cadetblue';
const beep = new Audio('data:audio/mp3;base64,SUQzAwAAAAABdlBSSVYAAAAOAABQZWFrVmFsdWUA/38AAFBSSVYAAAARAABBdmVyYWdlTGV2ZWwAeSkAAFRJVDIAAAAGAAAAUHVsc2VUQ09OAAAABQAAACgxMilUQUxCAAAADgAAAFNvbnkgRXJpY3Nzb25UUEUxAAAADgAAAFNvbnkgRXJpY3Nzb24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7cMAAAAyI7v+0l4ABxTNufzDWAjbdrtdtprEBnmvOoYuCBJGewFBIucigmhx74o8fv4+GBOIYyN5oHQyZY1er4ycHrLm5tisiZf3////pf0pKnzTR6IE3HGq4CsiZfv3lNXv6au8eUze9//////r536Upl48d/4fZ4YzYxZnZ3QhUaIRCQSKRWvBemkQEmUrty8GV5Nq0tXbJ6QQCg4SsuAszAPA4SXNx8HgS5maj3H9jpuqkXDSVhI036k/qZO2grU3qOvvvU31v6i5bt/60/9V+pm0/9Tf6yX1W+3/6H/+Z/1Gn////+UCol2MhBEiBSCSn18t2e5hk040M2YdyiSewijhmz//Up2oq690TET0AHhLG3///zoO5J/6//9Q/CNoo////OCJR/0v/9Y+kgl/p//9Y6v//+3LAJwAK3ZV5/ZaAEZSzLrmEnbLo//6ROJf/o9MqQiAgIiqqqs5RXghMCJsaWJEZdPWbbsAETF2NhAjWdubl3V1ekBJdyiRDJ7c/XJ6v//HvPdOgnANQ12MUm/+qKT+YD49FRGP//PfqPO3mN/+r9Co1T0M/+n7HP8yn/RvjxL/+JYUhADIgCVVak6YsRVSm2YBWpWDPPD8s5qifNhlQVwS3/SMQIjHa61faofyY/+sWlP/4rBJ//hK//0BP/8RP/xMT//FXZv4gGP9ODr/U7h3+dwn/5fE7ft+v1ysoZCREQFFVVHMMU/zCP0fQuoQWFJHgR3TMOaI6APBnCfHNPfs6KR0iqDv/qKI5pgXxNg2DZFFFZ2HiX/x1jl92OFIB44Nn/+hz/+hyowjkmT5xL/9W/shUSH/WGv/7cMBbgAtdpW3MzO3RjKavOPgdsvzsLcl+35DovKpRARcSRSSVItRKTxPhTvS+McGOx0lbli7CmAdCziJ9BNTMv2Xtet1NeuERQ7UFroJqRdk01X6v/6YIN26v3TQWk1TP37LQDCMi//9/sv+kxJhqb1f9L/67GCQ+hwM3////rc4a3EOykIgDKqrgbo3jIGOp08nWSPCYbSvCsHog2qLcBAIHkpFDZFB29u+yVtlqdE4MsOIxQNTJNjIshgQW0yZJFTrv69fWiZEsbLupdJd1oIjUG46KnX1/WskSVZ/1fd10lsF3NndfzzVt///6nSIpHZTmJdbQ////9SRp5MC5dmRBIhaqqmYdhaG5CLgrEMVDVAeMjyGdOm5gCawDZCcC0MeFv4XVu1Bq/f//Up6CCBiDbByGkwT/+3LAjoAM5b1n59GtwfC37jj5tboammnTL5fJMS8TM3oJvq+v1KaIQWJ+gh6m/ddmUyZIDkNEEKS1tWm6hkjzen2t/v/9SaaedN4/j0j8VDHZ/57ECKdmRCERFplqlJ8eompcVCkWCyte/FnzmJiRYjxZJQGaIMSoEfEoWiQZJ+vo1//r5kbEyapI6kkqJqk5MkVAvg4jE1R/9v1IqNlt/r/WjpF5IyAiZijU/etFjI4Ype3/st1N/U9kVhq03SM0VJf1E6h/+pv//6i+j//NwJYxEQABFVVWEVJOW8v43yxIY1ub+RWUhq8vqLhuTBOGYuclw2QDEIxOg7ySQQsZuXC+buo0ZN1Oh19SC01ILdNjMvuggmnTUYDgALYeBmbqToMtPTLjLTdBqdN0GWmk1TPUXDRlN1JrUf/7cMCvAA9BcXfHza2R6T1uOPnJuiYmg83QZb9BZmXzddP//+gXEEKbrTTUgaEmAbR9Bk01ILN3My+X3ISDdV9bpmC/rWyH/plFxB4AC3UxkRiRE1KJRdYkAMZdlpBnKqByodgpW4uUik069eqJwktPMsKjls96y7ySg4yIINRJdsu+rTQMLfNZmfgsG4LQXA+JK05TXHQgJDrHd800FCg2GptPqOkl/nOprCSb/U0dYJzZxymzaDw2G6P+/Qf/1Nyr+rHHmnIPM/+c80v7qP/7PqCFMhASERVqqp9VCNrTl7YDKlCgiCjf95j3/um0gPJ5LHUhcFRjWcauklum6avh481fMJrpq//6uQKF2A0P3PSsyfPgVRXEWLaf2Gw1PU98zyMTT0MujkAiCRovHLadpAWID3M9K0H/+3LAxoAURc9vx9Gt0hGxrrmGHbRWh556fRh9XMHhOnml/zE6E3/Qw9xunLVfq21MhEBIbr/+GWRXl1v63Ry5YI82CP3Sb7//9s0tBsAJ66nxqP6brmtvNHAjRH+d4iX3TO/v02QLF85+NLc4imnSYHYbMt+10UE2NzQJ6mh9Trq2TDlnqmvduyknE6ZK6v9TICyUurvfraOdS/dTIL61jiUgy/+p44El+3qalNf/9U7/QkCmQREAIBFWamuq3RtoLtPvSt2SqC2x60zz/66SVHOD1PBXA+pP9TokiMKSqkVJLHaJcSrK1OYj1LqKkVJUVJCzIVJJSTosplJLZSR0uhMxlEkPYlnRbv66KyIYqf+yVFjEep4yJ4niLK37omRdNaTooBy0kf1l1FG9lJJjDoPpLLprMUq2uv/7cMDFgA/5i3XMvU2SEjntuZe1uN1koU21o2mSKh64gf1eX9cugiIAYiKytOkzRuCfEraXfhhqBI9YSW5//TTQQWmYD0LhLnBAAjoN+tyYPQuIKQWmoJ7Q58gEQST2V0MUfgYBWQqgqA2DQ9/9lESTGGMf/954uG7p/+ee58Wf+swfnujMRiQTt9CAeHvq8xgvX9DGZfZXU8Sh03fETuBf6YsAhDAQAAABFVXTI4CWg+7pS2RGZGLDu43dq7XKqSLu1nY3CekZA6cTa6LnwUwRBDLCkFtG0+tT2SX7YqQQ9kITw+ANpNtl1u+SCIY6ep76mJcdD2ZuoqHVLm3VXE/E3aAereUDolzqupj6ody+138TDR3r1pJH6nvq/2T6T7lAsfz/f+ke99//Mm8VqG3yUWDpd3rGTnH/+3LA1QASvZdxzDWtkfkwbnmGqbIES7MyYGJIl31XOtHYbBj/urIH+qx2tSmTYamrODru7PbrdFa9isisJHgDrZBj1de+38ydRiYAZIOLGYbs9H9OutuEsWw2PV9bfvXtY4F3GC6nrpaudfV4Y67a91/zfU3JQQ9Jq1Muk7dRRP6lrw2UrofWjU/LJP/yyV3/v+bN/Isgh2RBISA6qr/6Bo7XW4rCvtH6aRy1lRhUC3bHCtWmr+kk+tEydACIAAaMRdB2/+r2sPkDAihYkVL9X627WcAugpJ8o6tf9FtGtRPB9Lqv2+o6m1JqJeDZC/lh9ST60m6nMtSSlqL4bRiqtdTpddInH9JNaROB6Eied2ZaST7sskRgn/RNv/+j/WYtmPLCQoi9Ki2smMw5x8M3knpiu8Bq6QOBRP/7cMDdABLdiW3MtW2SADzsebpBuNBOYLZJmUzvVWsipLGxKBqYB6J0vIqf9Jnr/UOUAtxspM7dHXatvs4AwgYqZqq11q19+vTJgLMyXY2ekvUt6R/XrOCEBmPPRXWp+y0SVb2cYoAgli6aqrKtXHclVpVAjxeXrqZJqWtSDfoJf/9T/yc4nXRjElZo3I42tdlq0o80Z4c4jRRJrIgaVknkXlIiK1utZkgtFFmSLJETYhwJcFaVTa7mz+g3oJrWozFkgPQh5q9IyLq0qCB15ii6dqDgB6ANVzWixjRbL6DInmUXKSbCXhJBg0GTNUTJ1VTjzhsk7O6ZwAK4BxGKlutTpPu6jIlnqdJSxYBLJNt/7tup0URijal6mX7zI1b84a/0+geSFS2XXXSEf71LqpmBIrQPepphE5H/+3LA44ARPedjzNGt0ic86fmotbycHJbs3zcmJOzBTjS61flL+2f3Vx1TUbk3VMiA4lEtUv6JT0RWFn5bS0spvSqXU2OE8zQ3FcmKALhgAaS7ZXqtmVzNXFmJmzWC9jNqGuMZqJ0bI/VyhNO3M0sWuLZe6/hPtQo1oydcFde+rZr/j1pb43mDXNYKtXSig6z///////8b/1CSgAAAAAD/+uV////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7cMDsABOps0fsRa3ia6lltYw9sf/////////////////////////////////////////////////////////////////////////////////////////////////8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+3LA4YAdGAMYgAAAIAAAJcAAAAT//////////////////////////////////////////////////////////////////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7cMD/gCSAAS4AAAAgAAAlwAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUQUdQdWxzZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABTb255IEVyaWNzc29uAAAAAAAAAAAAAAAAAAAAAABTb255IEVyaWNzc29u');
const uncalibratedReadOffset = logStatus => 'deductions' in logStatus
&& logStatus.deductions.some(RegExp.prototype.test.bind(/\b(?:read offset)\b/i));
const autoOpenTab = GM_getValue('auto_open_tab', true);
const editionSearch = GM_getValue('edition_search', true);
let discogsBindingsCache, amBindingsCache, mbInstrumentsCache;
const releaseTitleNorm = title => title && [
/\s+(?:EP|E\.\s?P\.|-\s+(?:EP|E\.\s?P\.|Single|Live))$/i,
/\s+\((?:EP|E\.\s?P\.|Single|Live)\)$/i, /\s+\[(?:EP|E\.\s?P\.|Single|Live)\]$/i,
].reduce((title, rx) => title.replace(rx, ''), title.trim());
const rxBracketStripper = (...patterns) => new RegExp('(?:\\s+(?:' + ['()', '[]'].map(function(br) {
const wc = `[^\\${br[0]}\\${br[1]}]*`;
const _patterns = patterns.map((pattern, index) => { if (pattern) switch (index) {
case 0: return `(?:${pattern})\\b${wc}`;
case 1: return `${wc}\\b(?:${pattern})\\b${wc}`;
case 2: return `${wc}\\b(?:${pattern})`;
} }).filter(Boolean);
if (_patterns.length > 0) return `\\${br[0]}(?:${_patterns.join('|')})\\${br[1]}`;
}).filter(Boolean).join('|') + '))+$', 'gi');
const trackTitleNorm = title => title && title.trim().replace(rxBracketStripper(
'original|club|feat(?:\\b|\\.|uring)|ft\\.?',
'live|(?:en|ao) (?:vivo|directo?)|unplugged|instrumental|acoustic',
'(?:re-?)?mix(?:ed)?|RMX|rework|edit|dub|version|demo'), '');
for (let tr of Array.prototype.filter.call(document.body.querySelectorAll('table#torrent_details > tbody > tr.torrent_row'),
tr => (tr = tr.querySelector('td > a')) != null && /\b(?:FLAC)\b.+\b(?:Lossless)\b.+\b(?:Log) \(\-?\d+\s*\%\)/.test(tr.textContent))) {
function makeConst(value) {
if (value == null || typeof value != 'object') return value;
const object = { };
for (let key in value) Object.defineProperty(object, key, { value: makeConst(value[key]), writable: false });
return object;
}
function addLookup(caption, callback, tooltip) {
const [span, a] = createElements('span', 'a');
span.className = 'brackets';
span.style = 'display: inline-flex; flex-flow: row; align-items: baseline; column-gap: 5px; justify-content: space-around; color: initial;';
if (caption instanceof Element) a.append(caption); else a.textContent = caption;
[a.className, a.href, a.onclick] = ['toc-lookup', '#', evt => { callback(evt); return false }];
if (tooltip) setTooltip(a, tooltip);
span.append(a);
container.append(span);
}
function svgCaption(resourceName, style, fallbackText) {
console.assert(resourceName || fallbackText);
if (!resourceName) return fallbackText;
let svg = new DOMParser().parseFromString(GM_getResourceText(resourceName), 'text/html');
if ((svg = svg.body.getElementsByTagName('svg')).length > 0) svg = svg[0]; else return fallbackText;
for (let attr of ['id', 'width', 'class', 'x', 'y', 'style']) svg.removeAttribute(attr);
if (style) svg.style = style;
svg.setAttribute('height', '0.9em');
return svg;
}
function imgCaption(src, style) {
const img = document.createElement('img');
img.src = src;
if (style) img.style = style;
return img;
}
function addIcon(html, clickHandler, dropHandler, className, style, tooltip, tooltipster = false) {
if (!html || ![clickHandler, dropHandler].some(cb => typeof cb == 'function')) throw 'Invalid argument';
const span = document.createElement('span');
span.innerHTML = html;
if (className) span.className = className;
span.style = 'transition: scale 100ms;' + (style ? ' ' + style : '');
if (typeof clickHandler == 'function') {
span.style.cursor = 'pointer';
span.onclick = function(evt) {
if (evt.currentTarget.disabled) return true; else evt.stopPropagation();
clickHandler(evt);
return false;
};
}
span.onmouseenter = span.onmouseleave = evt =>
{ evt.currentTarget.style.scale = evt.type == 'mouseenter' ? 1.5 : 'none' };
if (typeof dropHandler == 'function') {
span.ondragover = evt => Boolean(evt.currentTarget.disabled) || !evt.dataTransfer.types.includes('text/plain');
span.ondrop = function(evt) {
evt.currentTarget.style.scale = 'none';
if (evt.currentTarget.disabled || !evt.dataTransfer || !(evt.dataTransfer.items.length > 0)) return true;
let urls = evt.dataTransfer.getData('text/plain');
if (urls && (urls = urls.split(/(?:\r?\n)+/).map(url => url.trim())).length > 0) evt.stopPropagation(); else return;
dropHandler(evt, urls);
return false;
}
span.ondragenter = span[`ondrag${'ondragexit' in span ? 'exit' : 'leave'}`] = function(evt) {
if (evt.currentTarget.disabled) return true;
if (!evt.currentTarget.contains(evt.relatedTarget))
evt.currentTarget.style.scale = evt.type == 'dragenter' ? 3 : 'none';
return false;
};
}
if (tooltip) if (tooltipster) setTooltip(span, tooltip); else span.title = tooltip;
return span;
}
function getReleaseYear(date) {
if (!date) return undefined;
let year = new Date(date).getUTCFullYear();
return (!isNaN(year) || (year = /\b(\d{4})\b/.exec(date)) != null
&& (year = parseInt(year[1]))) && year >= 1900 ? year : NaN;
}
function svgSetTitle(elem, title) {
if (!(elem instanceof Element)) return;
for (let title of elem.getElementsByTagName('title')) title.remove();
if (title) elem.insertAdjacentHTML('afterbegin', `<title>${title}</title>`);
}
function mbFindEditionInfoInAnnotation(elem, mbId) {
if (!mbId || !(elem instanceof HTMLElement)) throw 'Invalid argument';
return mbApiRequest('annotation', { query: `entity:${mbId} AND type:release` }).then(function(response) {
if (response.count <= 0 || (response = response.annotations.filter(function(annotation) {
console.assert(annotation.type == 'release' && annotation.entity == mbId, 'Unexpected annotation for MBID %s:', mbId, annotation);
return /\b(?:Label|Catalog|Cat(?:alog(?:ue)?)?\s*(?:[#№]|Num(?:ber|\.?)|(?:No|Nr)\.?))\s*:/i.test(annotation.text);
})).length <= 0) return Promise.reject('No edition info in annotation');
const a = document.createElement('a');
[a.href, a.target, a.textContent, a.style] = [[mbOrigin, 'release', mbId].join('/'),
'_blank', 'by annotation', 'font-style: italic; ' + noLinkDecoration];
a.title = response.map(annotation => annotation.text).join('\n');
elem.append(a);
});
}
function editionInfoMatchingStyle(elem) {
if (!(elem instanceof Element)) throw 'Invalid argument';
//elem.style.fontWeight = 'bold';
elem.style.textShadow = '0 0 1pt #aaa';
//elem.style.textShadow = '0 0 5pt #9acd32C0';
elem.style.textDecoration = 'underline yellowgreen dotted 2pt';
//elem.style.backgroundColor = '#9acd3230';
elem.classList.add('matching');
}
function releaseEventMapper(countryCode, date, editionYear) {
if (!countryCode && !date) return null;
const components = [ ];
if (countryCode) {
const [span, img] = createElements('span', 'img');
span.className = 'country';
if (/^[A-Z]{2}$/i.test(countryCode)) {
[img.height, img.referrerPolicy, img.title] = [9, 'same-origin', countryCode.toUpperCase()];
img.setAttribute('onerror', 'this.replaceWith(this.title)');
img.src = 'http://s3.cuetools.net/flags/' + countryCode.toLowerCase() + '.png';
span.append(img);
} else span.textContent = countryCode;
components.push(span);
}
if (date) {
const span = document.createElement('span');
[span.className, span.textContent] = ['date', date];
if (editionYear > 0 && editionYear == getReleaseYear(date.toString())) editionInfoMatchingStyle(span);
components.push(span);
}
return components;
}
function catNoMapper(catNo) {
if (catNo) catNo = dashUnifier(catNo); else return [ ];
const m = rxCatNoRange.exec(catNo);
if (m == null) return [catNo]; else if (m[3].length > m[2].length) return [m[1] + m[2]];
catNo = [ ];
for (let n = m[2]; n <= m[2].slice(0, -m[3].length) + m[3]; ++n) catNo.push(m[1] + n);
return catNo.length > 0 ? catNo : [m[1] + m[2]];
}
function editionInfoParser(torrent) {
const [labels, catNos] = ['RecordLabel', 'CatalogueNumber'].map(prop => (value => value ? decodeHTML(value)
.split(rxEditionSplitter).map(value => value.trim()).filter(Boolean) : [ ])(torrent['remaster' + prop]));
return [
labels.map(label => !rxNoLabel.test(label) ? labelMapper(label.replace(...rxBareLabel)) : noLabel),
Array.prototype.concat.apply([ ], catNos.map(catNo => !rxNoCatno.test(catNo) ? catNoMapper(catNo) : [ ])),
].map(values => values.filter((s1, n, a) => a.findIndex(s2 => s2.toLowerCase() == s1.toLowerCase()) == n));
}
function editionInfoMapper(labelName, catNo, recordLabels, catalogueNumbers, labelURL) {
if (!labelName && !catNo) return null;
const components = [ ];
if (labelName) {
const elem = document.createElement(labelURL ? 'a' : 'span');
[elem.className, elem.textContent] = ['label', dashUnifier(labelName)];
if (labelURL) [elem.href, elem.target, elem.style] = [labelURL, '_blank', noLinkDecoration];
labelName = labelMapper(labelName.replace(...rxBareLabel));
if (Array.isArray(recordLabels) && recordLabels.some(function(recordLabel) {
const labels = [recordLabel, labelName].map(label => rxNoLabel.test(label) ? noLabel : label);
const startsWith = (index1, index2) => labels[index1].toLowerCase().startsWith(labels[index2].toLowerCase())
&& /^\W/.test(labels[index1].slice(labels[index2].length));
return cmpNorm(labels[0]) == cmpNorm(labels[1]) || startsWith(0, 1) || startsWith(1, 0);
})) editionInfoMatchingStyle(elem);
components.push(elem);
}
if (catNo) {
const span = document.createElement('span');
[span.className, span.textContent, span.style] = ['catno', dashUnifier(catNo), 'white-space: nowrap;'];
catNo = catNoMapper(catNo);
if (Array.isArray(catalogueNumbers) && (catalogueNumbers.some(catalogueNumber =>
catNoMapper(catalogueNumber).some(catalogueNumber =>
catNo.some(catNo => sameStringValues(catalogueNumber, catNo)))
|| catNo.some(catNo => sameStringValues(catNo, catalogueNumbers.join('/'))))))
editionInfoMatchingStyle(span);
components.push(span);
}
return components;
}
function fillListRows(container, listElements, maxRowsToShow, expandedIfMatch = false) {
function addRows(root, range) {
for (let row of range) {
const div = document.createElement('div');
row.forEach((elem, index) => { if (index > 0) div.append(' '); div.append(elem) });
root.append(div);
}
}
if (!(container instanceof HTMLElement)) throw 'Invalid argument';
if (!Array.isArray(listElements) || (listElements = listElements.filter(listElement =>
Array.isArray(listElement) && listElement.length > 0)).length <= 0) return;
addRows(container, maxRowsToShow > 0 ? listElements.slice(0, maxRowsToShow) : listElements);
if (!(maxRowsToShow > 0 && listElements.length > maxRowsToShow)) return;
const hasMatching = commponents => commponents.some(component => component.classList.contains('matching'));
if (expandedIfMatch && !listElements.slice(0, maxRowsToShow).some(hasMatching)
&& listElements.slice(maxRowsToShow).some(hasMatching))
return addRows(container, listElements.slice(maxRowsToShow));
const divs = createElements('div', 'div');
[divs[0].className, divs[0].style] = ['show-all', 'color: cadetblue; font-style: italic; cursor: pointer;'];
[divs[0].onclick, divs[0].textContent, divs[0].title] = [function(evt) {
evt.currentTarget.remove();
divs[1].hidden = false;
}, `+ ${listElements.length - maxRowsToShow} others…`, 'Show all'];
divs[1].hidden = true;
addRows(divs[1], listElements.slice(maxRowsToShow));
container.append(...divs);
}
function discogsIdExtractor(expr, entity) {
if (!expr || !entity) return null;
let discogsId = parseInt(expr);
if (discogsId > 0) return discogsId; else try { discogsId = new URL(expr) } catch(e) { return null }
return discogsId.hostname.endsWith('discogs.com')
&& (discogsId = new RegExp(`\\/${entity}s?\\/(\\d+)\\b`, 'i').exec(discogsId.pathname)) != null
&& (discogsId = parseInt(discogsId[1])) > 0 ? discogsId : null;
}
function allMusicIdExtractor(expr, entity) {
if (!expr) return null;
let allMusicId = /^(m[a-z]\d{10})$/i.exec(expr);
if (allMusicId != null) return allMusicId[1].toLowerCase();
try { allMusicId = new URL(expr) } catch(e) { return null }
return allMusicId.hostname.endsWith('allmusic.com') && (!entity
|| allMusicId.pathname.startsWith('/' + entity.toLowerCase() + '/'))
&& (allMusicId = /\b(m[a-z]\d{10})\b/i.exec(allMusicId.pathname)) != null ?
allMusicId[1].toLowerCase() : null;
}
function isDiscogsCD(format) {
const descriptions = getFormatDescriptions(format);
return ['CD', 'CDr'].includes(format.name) && !descriptions.some(description =>
['SVCD', 'VCD', 'CDi'].includes(description))
|| format.name == 'Hybrid' && descriptions.includes('DualDisc')
|| format.name == 'SACD' && descriptions.includes('Hybrid');
}
function findDiscogsRelatives(entity, discogsId) {
if (!entity || !((discogsId = parseInt(discogsId)) > 0)) throw 'Invalid argument';
const targetType = entity.replace(/-/g, '_');
return mbApiRequest('url', {
query: `url_descendent:*discogs.com/${discogsEntity(entity)}/${discogsId}`,
targettype: targetType,
limit: 100,
}).then(results => results.count > 0 && (results = results.urls.filter(url =>
discogsIdExtractor(url.resource, discogsEntity(entity)) == discogsId)).length > 0 ? Promise.all(results.map(url =>
mbApiRequest('url/' + url.id, { inc: entity + '-rels' }).then(url => url.relations.filter(relation =>
relation['target-type'] == targetType), console.warn))) : [ ])
.then(relations => (relations = Array.prototype.concat.apply([ ], relations.filter(Boolean)).map(relation =>
relation[relation['target-type']]).filter((entry1, index, array) => array.findIndex(entry2 =>
entry2.id == entry1.id) == index)).length > 0 ? relations : Promise.reject('No relations by URL'));
}
function findAllMusicRelatives(entity, allMusicId, mbEntity = entity) {
if (!entity || !allMusicId || !mbEntity) throw 'Invalid argument';
const targetType = mbEntity.replace(/-/g, '_');
return mbApiRequest('url', {
query: `url_descendent:*allmusic.com/${entity}/*${allMusicId}`,
targettype: targetType,
limit: 100,
}).then(results => results.count > 0 && (results = results.urls.filter(url =>
allMusicIdExtractor(url.resource, entity) == allMusicId)).length > 0 ? Promise.all(results.map(url =>
mbApiRequest('url/' + url.id, { inc: mbEntity + '-rels' }).then(url => url.relations.filter(relation =>
relation['target-type'] == targetType), console.warn))) : [ ])
.then(relations => (relations = Array.prototype.concat.apply([ ], relations.filter(Boolean))).length > 0 ?
relations.map(relation => relation[relation['target-type']]).filter((entry, index, array) =>
array.findIndex(entry2 => entry2.id == entry.id) == index) : Promise.reject('No relations by URL'));
}
function appendDisambiguation(elem, disambiguation) {
if (!(elem instanceof HTMLElement) || !disambiguation) return;
const span = document.createElement('span');
[span.className, span.style.opacity, span.textContent] =
['disambiguation', 0.6, '(' + disambiguation + ')'];
elem.append(' ', span);
}
function addThumbnail(element, src, url) {
function setThumbNail(src) {
if (src) [img.onload, img.onerror, img.src] = [function(evt) {
function addHoverHandlers(elem) {
elem.style.transition = 'scale 200ms ease-in-out';
elem.style.boxSizing = 'border-box';
elem.onmouseenter = elem.onmouseleave = function(evt) {
evt.currentTarget.style.scale = evt.type == 'mouseenter' ? 8 : 'none';
evt.currentTarget.style.border = evt.type == 'mouseenter' ? '1pt solid #aaaa' : null;
};
if (url) elem.onclick = function(evt) {
evt.stopPropagation();
GM_openInTab(url, false);
}; else return;
elem.style.cursor = 'pointer';
}
if (evt.currentTarget.src != defaultSrc) /*if (evt.currentTarget.naturalWidth == evt.currentTarget.naturalHeight) {
const canvas = document.createElement('canvas'), context = canvas.getContext('2d');
[canvas.className, canvas.style] = ['rectangle-cover-icon', 'height: 10px; margin-right: 3pt;'];
[canvas.width, canvas.height] = [evt.currentTarget.naturalWidth, evt.currentTarget.naturalHeight];
context.drawImage(evt.currentTarget, 0, 0);
const shortest = Math.min(evt.currentTarget.naturalWidth, evt.currentTarget.naturalHeight);
let scale = 0.6;
const offsetX = Math.round((1 - scale) * (evt.currentTarget.naturalWidth - shortest / 8));
const offsetY = Math.round(shortest * (1 - scale) / 8);
const path = new Path2D('M25.62 16.68c8.5,10.59 17.2,20.68 26.26,30.72 3.45,-2.66 6.81,-5.38 10.02,-8.18 4.02,-3.37 9.55,-10.75 12.7,-15.08 2.37,-3.17 4.57,-6.32 6.75,-9.62 0.8,-1.34 1.56,-2.69 2.33,-4.06 0.38,-0.82 1.29,-3.35 2.15,-3.71 1.7,-0.25 2.2,3.33 2.29,4.38 0.21,1.99 0,3.73 -0.38,5.69 -0.78,4.46 -2.28,8.35 -4.08,12.48 -3.14,6.64 -6.76,13.06 -10.83,19.17 -2.9,3.79 -6.26,7.47 -9.92,11.01 6.3,6.8 12.67,13.5 19.16,20.21 3.53,3.4 7.1,6.72 10.67,10.08 1.17,0.93 7.09,4.96 7.26,6.36 -0.65,3.15 -9.17,-0.1 -10.58,-0.68 -5.02,-1.87 -9.44,-4.58 -13.89,-7.52 -8.6,-5.56 -16.46,-11.82 -24.08,-18.49 -6.18,4.9 -12.64,9.37 -18.74,13.28 -10.44,6.16 -22.82,16.29 -25.99,3.68 -4.12,-11.03 3.01,-10.99 16.3,-19.17 5.05,-3.19 10.17,-6.51 15.24,-9.98 -9.51,-9.27 -18.49,-18.77 -26.91,-29.16 -7.51,-9.88 -18.05,-19.86 -5.6,-23.85 10.84,-4.5 9.95,-0.13 19.87,12.44z');
context.setTransform(scale *= shortest / 100, 0, 0, scale, offsetX, offsetY);
context.fillStyle = '#f00a';
context.fill(path);
addHoverHandlers(canvas);
evt.currentTarget.replaceWith(canvas);
} else */addHoverHandlers(evt.currentTarget);
}, evt => { evt.currentTarget.src = defaultSrc }, src];
}
if (!(element instanceof HTMLElement)) throw 'Invalid argument';
if (typeof src == 'string' && src.endsWith('/images/spacer.gif')) return;
const defaultSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsSAAALEgHS3X78AAAGjUlEQVR4nO1aXUgcVxS+6qghbvxBRROyIaLF1SSYEPGXNWmigVJMIJA2IIQ89KHQ0pc+BdK+5aVt+kNKoYRCoZiYtEINClkT1KioqIkW0gSrKBr/uiT1LzFZo27Pd8ldNpNdZ3Z3xtkxfjC4u565c853zj3n3DtXcrvd7G2GZLQCRmOTAKMVMBqbBBitgNHwENDb2/tRe3v753NzcxlUGUxPTERExGpcXNxUfn7+z3a7/WJUVJTLlxw3tKOj47P6+vofIiMjGQmur6Y6gZwYRc7cdfPmzQvz8/PWysrKT0CKXE4aHh5+t7W19TwMBwGrq6tsZWWFmb0/gD24YmJiWHd398e5ubl/ZmVlOeRyUltb27nFxcVUYXxKSgojQYSQEXprhkePHrHx8XHuVDj04cOHJ3wS8Pjx42xhLAg4fPgwy8vLW3eFtQaMv3z5Mo9kkPD06dMMX3LIAZ55ASKio6PXTUk9IUkSt0dMZcx/l8tlef78eXJiYuKoR05+o9nnvoDcDlSBkZGRomvXrv2RkZHRXl5efn7Hjh39AZW7Z8+escHBQeZ0OvkDUlNTeb6Ij4/XVHkdEUFRkED54P2JiYn8M2fOvKeagL6+Pnb79m02MzPjHVbceOSNoqIi3bTWCqR3JP6iMiwsLKRRibyoioCenh5WV1fHP8tzBKLixo0bjJhlhw4d0lpnzQDjt23b5ty6devM0tJSEuyYnJwsUiSAqgRzOBzc28imcuA3/K+pqYllZmaynTt36mJAqKBSGEO69RUUFFS3tLR8CgLwmyIB/f393MsIG38AAcQqu3v3btgSILBly5ZFr6/KPf/U1JRPz8uBrmt6eprnh1CbKGph2cDAACNvhTSOL5B+rymnSMDy8rLqwSEbKgGUnNjVq1cZlSxOREVFRdBjqYEiAZQ4VPUGkLFYLKqixR+E8WNjYyw2NpbRXOVkUs0OekwlKBKQnZ3N7t27pzgQ2mjIBgsYX11dzY0XlQbTqrm5mZOrVyQoEpCTk8ObHTRA/hLhy5cvmdVqZfv37w9KCRh/5cqV14wH4H2QgEgA9CBBkQD01CdPnmQ1NTVsdHSUiWUzIJbO27dvZ6dOnUKGDVgBX573hiABkQBoTYKqRogWD+zs2bOsq6uL3b9/nycnAPnBZrOx0tJSFhcXF/DD/XleDpAAR+gRCapbYXgXLa/dbsfSks9LGB3s6lHJ83LoNR0C3vuDEgkJCSE9VK3n5dCDhHXf/AzU83JonRPWlYD5+XlPnQ9l40XLnBAyAagE9fX1nmWxP8D4YMLeH7ynA/LRsWPHghonJAJgfENDA+vs7OSlEVdZWdkbct4dnpZbboKEO3fu8GcfPXo04DY8aAK8jUeDBC80NjZyBVApBLT2vBximY5VK56LFjoQBEWAt/HCKKEI9g5ABiJBL8/7QrAvdAImQMx5NEVyowQJt27d4jtE6BxxhfNOc0AECM/7Ml4AJOBCcgIZ4Ww8oJqAtTzvc2DJHO9XVWmpxvNmhSIBWO1tVOMBRQLwLkCUuo0IRQJQwkLZ5gp3KBKwUQ5M+IM5UrWO2CRASQB7/XjrE86vzVGmsTEbDBQJKCws5Nvd4ZwI4Rxs2QXTfCnesRGOy6yFNwgw++EoAbV2SCToiW2E0osXL3RTaj2BvIXcoAQpLS3t79nZ2V1iKYvNxuHhYdNHwuTkpOeEGIiIj4//B7/Lk7lUVlb25cTExN6FhQUrhIkM9uTJE0OU1hLiDRYMpuS4vGfPnlpfcpLVau2tqKg4V1tb+yuVPAk3boRFDwzHQg7XkSNHLuzevbvNlxxPggcOHKgmlpZaWlq+IO/n0E3of900DdyyQTEvTDE3yPurSUlJo8XFxd+VlJT86E/OUwX27dv3u81mayAC3nG5XHzph4h49W9ORGxs7KKPMcISFMUrycnJI+TY13QmYla8v0uymxbT09P/Et+vX7/+2/T0tI1uctO1TPni+5SUlAF9VdcGOPLvdDpzKAGiyiFq3fQ5emhoqEQ0dfwFy1qD4EDR+Ph4PraaMadqamqKKUesys/ZmAnQHXkOlcFisfy7JgE0d7558ODBcaoQ6V5tZqSZS6Q4P4w1TkFBwaU1CaBwHzx9+vSHdXV1lyg37PVumsyKV2VxrrS09Ce73f6t4lqAykdrVVXVBw6H4yuaP4W06oowcQREUGL8jwz/+uDBg7/gB1XLJyQ+IuGEvroZg80NEaMVMBqbBBitgNF46wn4H+FgCXJk4OBeAAAAAElFTkSuQmCC';
const img = document.createElement('img');
[img.height, img.style.marginRight, img.className] = [10, '3pt', 'cover-icon'];
element.insertAdjacentElement('afterbegin', img);
if (src instanceof Promise) {
img.src = defaultSrc;
src.then(setThumbNail);
} else if (src) setThumbNail(src); else img.src = defaultSrc;
}
function setEditionInfo(elem, editionInfo) {
if (!(elem instanceof HTMLElement)) throw 'Invalid argument';
if (!Array.isArray(editionInfo) || editionInfo.length <= 0) return;
const uniqueValues = (el1, ndx, arr, normFn = cmpNorm) =>
el1 && arr.findIndex(el2 => el2 && normFn(el2) == normFn(el1)) == ndx;
elem.dataset.remasterRecordLabel =
editionInfo.map(labelInfo => labelMapper(labelInfo.label)).filter((label, index, labels) =>
uniqueValues(label, index, labels, label => cmpNorm(label.replace(...rxBareLabel)))).join(' / ');
elem.dataset.remasterCatalogueNumber =
editionInfo.map(labelInfo => dashUnifier(labelInfo.catNo)).filter((catNo, index, catNos) =>
uniqueValues(catNo, index, catNos, catNo => cmpNorm(catNo.replace(rxCatNoRange, '$1$2')))).join(' / ');
}
function setMusicBrainzArtist(release, artist, linkify = true) {
if ('artist-credit' in release) release['artist-credit'].forEach(function(artistCredit, index, artists) {
if (linkify && 'artist' in artistCredit && artistCredit.artist.id && ![mb.spa.VA].includes(artistCredit.artist.id)) {
const a = document.createElement('a');
if (artistCredit.artist) a.href = [mbOrigin, 'artist', artistCredit.artist.id].join('/');
[a.target, a.style, a.textContent, a.className] =
['_blank', noLinkDecoration, artistCredit.name, 'musicbrainz-artist'];
if (artistCredit.artist) a.title = artistCredit.artist.disambiguation || artistCredit.artist.id;
artist.append(a);
} else artist.append(artistCredit.name);
if (artistCredit.joinphrase) artist.append(artistCredit.joinphrase);
else if (index < artists.length - 1) artist.append(index < artists.length - 2 ? ', ' : ' & ');
});
}
function setMusicBrainzTitle(release, title) {
title.innerHTML = linkHTML([mbOrigin, 'release', release.id].join('/'), release.title, 'musicbrainz-release');
if (release['cover-art-archive'] && release['cover-art-archive'].artwork) addThumbnail(title,
globalXHR('https://coverartarchive.org/release/' + release.id, { responseType: 'json' }).then(function({response}) {
const isFront = image => image.front || image.types && image.types.includes('Front');
let thumbnail = response.images.find(image => isFront(image) && image.types.length == 1)
|| response.images.find(isFront) || response.images[0];
if (thumbnail) thumbnail = thumbnail.thumbnails && thumbnail.thumbnails.small || thumbnail.image;
return thumbnail;
}), [mbOrigin, 'release', release.id, 'cover-art'].join('/'));
switch (release.quality) {
case 'low': title.insertAdjacentHTML('afterbegin', svgBulletHTML('#ff6723')); break;
case 'high': title.insertAdjacentHTML('afterbegin', svgBulletHTML('#00d26a')); break;
}
appendDisambiguation(title, release.disambiguation);
}
function setMusicBrainzReleaseEvents(release, releaseEvents, releaseYear) {
if ('release-events' in release) {
fillListRows(releaseEvents, Array.prototype.concat.apply([ ], release['release-events'].map(function(releaseEvent) {
const countryEvents = releaseEvent.area && Array.isArray(releaseEvent.area['iso-3166-1-codes']) ?
iso3166ToFlagCodes(releaseEvent.area['iso-3166-1-codes']).map(countryCode =>
releaseEventMapper(countryCode, releaseEvent.date, releaseYear)).filter(Boolean) : [ ];
return countryEvents.length > 0 ? countryEvents : releaseEvent.country || releaseEvent.date ?
iso3166ToFlagCodes([releaseEvent.country]).map(countryCode =>
releaseEventMapper(countryCode, releaseEvent.date, releaseYear)) : null;
}).filter(Boolean)), 3);
}
if (releaseEvents.childElementCount <= 0) fillListRows(releaseEvents,
iso3166ToFlagCodes([release.country]).map(countryCode =>
releaseEventMapper(countryCode, release.date, releaseYear)));
}
function setMusicBrainzTooltip(release, elem) {
function applyTooltip() {
elem.title = lines.map(lines => lines.filter(Boolean).join('\n')).filter(Boolean).join('\n');
}
if (!release || !(elem instanceof HTMLElement)) throw 'Invalid argument';
if (elem.title) elem = elem.querySelector('td.title > a.musicbrainz-release');
const lines = [[ ], [ ], [ ]], relations = [ ];
if (release['release-group']) lines[0].push([release['release-group']['primary-type']]
.concat((release['release-group']['secondary-types'] || [ ])).filter(Boolean).join(' + '));
if (release.media) lines[0].push(release.media.map(medium => medium.format || 'unknown medium').join(' + '));
const getSeries = root => root && root.relations ? root.relations
.filter(relation => relation['target-type'] == 'series').map(function(relation) {
let line = relation.type + ': ' + relation.series.name;
if (relation['attribute-values'] && relation['attribute-values'].number)
line += ' (' + relation['attribute-values'].number + ')';
return line;
}) : [ ];
const hasRelation = (entity, targetType) => Boolean(entity.relations)
&& entity.relations.some(relation => ['artist', 'label', 'place'].includes(relation['target-type']));
if (release.relations) lines[1] = getSeries(release);
if (hasRelation(release)) relations.push('release');
if (hasRelation(release?.['release-group'])) relations.push('release');
if (release.media && release.media.some(medium => medium.tracks
&& medium.tracks.some(track => hasRelation(track.recording)))) relations.push('recording');
if (release.media && release.media.some(medium => medium.tracks && medium.tracks.some(track =>
track.relations && track.relations.some(relation =>
relation['target-type'] == 'work' && hasRelation(relation.work))))) relations.push('work');
if (relations.length > 0) lines[1].push('Relationships: ' + relations.join(', '));
lines[2].push([release.status, release.packaging].filter(Boolean).join(' / '));
if (release.quality && release.quality != 'normal') lines[2].push(release.quality + ' data quality');
lines[2].push(release.id);
applyTooltip();
if (release['release-group']) mbApiRequest('release-group/' + release['release-group'].id, {
inc: 'releases+media+discids+series-rels',
}).then(function(releaseGroup) {
const series = getSeries(releaseGroup);
if (series.length <= 0) return;
Array.prototype.unshift.apply(lines[1], series);
applyTooltip();
});
}
function setMusicBrainzGroupSize(release, groupSize, releasesWithId, totalDiscs = 0) {
if (release['release-group']) mbApiRequest('release-group/' + release['release-group'].id, {
inc: 'releases+media+discids+series-rels',
}).then(function(releaseGroup) {
const releases = releaseGroup.releases.filter(release => !release.media || !totalDiscs
|| sameMedia(release).length == totalDiscs);
const a = document.createElement('a');
a.href = [mbOrigin, 'release-group', release['release-group'].id/*, 'releases'*/].join('/');
[a.target, a.style, a.textContent] = ['_blank', noLinkDecoration, releases.length];
if (releases.length == 1) a.style.color = '#0a0';
groupSize.append(a);
groupSize.title = 'Same media count in release group';
const counts = ['some', 'every'].map(fn => releases.filter(release => release.media
&& (release = sameMedia(release)).length > 0
&& release[fn](medium => medium.discs && medium.discs.length > 0)).length);
releasesWithId.textContent = counts[0] > counts[1] ? counts[0] + '/' + counts[1] : counts[1];
releasesWithId.title = 'Same media count with known TOC in release group';
}, function(reason) {
if (releasesWithId.parentNode != null) releasesWithId.remove();
[groupSize.colSpan, groupSize.innerHTML, groupSize.title] = [2, svgFail(), reason];
});
}
function setDiscogsArtist(artist, artists) {
if (!(artist instanceof HTMLElement)) throw 'Invalid argument';
if (artists) artists.forEach(function(artistCredit, index, artists) {
const name = creditedName(artistCredit);
if (artistCredit.id > 0 && ![194].includes(artistCredit.id)) {
const a = document.createElement('a');
if (artistCredit.id) a.href = [dcOrigin, 'artist', artistCredit.id].join('/');
[a.target, a.style, a.className, a.title] =
['_blank', noLinkDecoration, 'discogs-artist', artistCredit.role || artistCredit.id];
a.textContent = name;
artist.append(a);
} else artist.append(name);
if (artistCredit.join) artist.append(fmtJoinPhrase(artistCredit.join));
else if (index < artists.length - 1) artist.append(index < artists.length - 2 ? ', ' : ' & ');
});
}
function discogsSeriesMapper(series) {
if (!series || !series.name) return;
let result = 'Series: ' + stripDiscogsNameVersion(series.name);
if (series.catno) result += ' (' + series.catno + ')';
return result;
}
function discogsIdentifierMapper(identifier) {
let label = identifier.type;
if (identifier.description) label += ' (' + identifier.description + ')';
return label + ': ' + identifier.value;
}
function setDiscogsTooltip(release, elem) {
if (!release || !(elem instanceof HTMLElement)) throw 'Invalid argument';
if (elem.title) elem = elem.querySelector('td.title > a.discogs-release');
const lines = [ ];
const singleLineAdapter = (props, valueMapper = val => val) =>
[props.map(prop => release[prop] && valueMapper(release[prop])).filter(Boolean).join(' / ')];
if (release.formats) lines.push(release.formats.map(function(format) {
const tags = getFormatDescriptions(format);
if (format.name == 'All Media') return tags.join(', ');
let description = format.qty + '×' + format.name;
if (tags.length > 0) description += ' (' + tags.join(', ') + ')';
return description;
}));
if (release.series) lines.push(release.series.map(discogsSeriesMapper));
if (release.identifiers) lines.push(release.identifiers.map(discogsIdentifierMapper));
if (release.notes) lines.push(release.notes.split(/(?:\r?\n)+/).map(line => line.trim()));
if (release.extraartists) lines.push(release.extraartists.map(function(artistCredit) {
let line = artistCredit.name + ': ' + artistCredit.role;
if (artistCredit.tracks) line += ' (' + artistCredit.tracks + ')';
return line;
}));
if (release.companies) {
const r = { };
for (let company of release.companies) {
if (!(company.entity_type_name in r)) r[company.entity_type_name] = [ ];
let name = company.name;
if (company.catno) name += ' (' + company.catno + ')';
r[company.entity_type_name].push(name);
}
lines.push(Object.keys(r).map(key => key + ': ' + r[key].join(', ')));
}
lines.push(singleLineAdapter(['genres', 'styles'], val => val.join(', ')));
//lines.push(singleLineAdapter(['data_quality', 'status']));
//lines.push(['ID ' + release.id]);
elem.title = lines.map(function(lines) {
if ((lines = lines.filter(Boolean)).length > 15)
lines = lines.slice(0, 15).concat(`…and ${lines.length - 1} more`);
return lines.length > 0 ? lines.join('\n') : undefined;
}).filter(Boolean).join('\n\n');
}
function setDiscogsGroupSize(release, groupSize) {
if (!release || !(groupSize instanceof HTMLElement)) throw 'Invalid argument';
if (release.master_id) {
const masterUrl = new URL('master/' + release.master_id, dcOrigin);
for (let format of ['CD', 'CDr']) masterUrl.searchParams.append('format', format);
masterUrl.hash = 'versions';
const getGroupSize1 = () => dcApiRequest(['masters', release.master_id, 'versions'].join('/'))
.then(({filters}) => (filters = filters && filters.available && filters.available.format) ?
['CD', 'CDr'].reduce((s, f) => s + (filters[f] || 0), 0) : Promise.reject('Filter totals missing'));
const getGroupSize2 = (page = 1) => dcApiRequest(['masters', release.master_id, 'versions'].join('/'), {
page: page,
per_page: 1000,
}).then(function(versions) {
const releases = versions.versions.filter(version => !Array.isArray(version.major_formats)
|| version.major_formats.some(format => ['CD', 'CDr'].includes(format))
|| version.major_formats.includes('Hybrid') && version.format.includes('DualDisc')
|| version.major_formats.includes('SACD') && version.format.includes('Hybrid')).length;
if (!(versions.pagination.pages > versions.pagination.page)) return releases;
return getGroupSize2(page + 1).then(releasesNxt => releases + releasesNxt);
});
getGroupSize1().catch(reason => getGroupSize2()).then(function(_groupSize) {
const a = document.createElement('a');
[a.href, a.target, a.style, a.textContent] = [masterUrl, '_blank', noLinkDecoration, _groupSize];
if (_groupSize == 1) a.style.color = '#0a0';
groupSize.append(a);
groupSize.title = 'Total of same media versions for master release';
}, function(reason) {
[groupSize.style.paddingTop, groupSize.innerHTML, groupSize.title] = ['5pt', svgFail(), reason];
});
} else [groupSize.textContent, groupSize.style.color, groupSize.title] =
['–', '#0a0', 'Without master release'];
}
function getDiscogsReleaseDescriptors(release) {
let descriptors = new Set;
if (release.formats) for (let format of release.formats) {
if (!['CD', 'CDr', 'SACD', 'Hybrid', 'All Media'].includes(format.name)) continue;
const descriptions = getFormatDescriptions(format);
if (!descriptions.some(description => ['SVCD', 'VCD', 'CDi'].includes(description))
&& (format.name != 'Hybrid' || !descriptions.includes('DualDisc'))
&& (format.name != 'SACD' || !descriptions.includes('Hybrid')))
for (let description of descriptions) if (![
'Album', 'Single', 'EP', 'LP', 'Compilation', 'Stereo',
].includes(description)) descriptors.add(description);
}
return Array.from(descriptors);
}
function addResultsFilter(thead, tbody, minRows = 5) {
function filterByClasses(...classes) {
if (classes.length > 0) classes = classes.map(cls => ({
catno: ['catno', 'barcode', 'identifier'],
}[cls]) || [cls]); else return null;
const rows = Array.prototype.filter.call(tbody.rows, (tr, index) => classes.every(cls =>
cls.some(cls => tr.querySelector('.' + cls + '.matching') != null)
|| classes.length > 1 && cls.includes('date') && tr.querySelector('.' + cls) == null));
return rows.length > 0 ? rows : null;
}
if (!(thead instanceof HTMLElement) || !(tbody instanceof HTMLElement)) throw 'Invalid argument';
if (minRows > 0 && tbody.rows.length < minRows) return;
const filteredRows = filterByClasses('date', 'label', 'catno') || filterByClasses('label', 'catno')
|| filterByClasses('catno') || filterByClasses('label') || filterByClasses('date');
if (filteredRows == null || filteredRows.length >= tbody.rows.length) return;
const [labels, cls] = [['Relevant Editions Only', 'Show All'], 'filtered'];
thead.append(Object.assign(document.createElement('span'), {
style: 'float: right; color: cadetblue; cursor: pointer; text-transform: lowercase;',
className: 'filter-switch',
textContent: '[' + labels[0] + ']',
onclick: function(evt) {
const filtered = tbody.classList.contains(cls);
for (let tr of tbody.rows) tr.hidden = !filtered && !filteredRows.includes(tr);
evt.currentTarget.textContent = '[' + labels[filtered ? 0 : 1] + ']';
tbody.classList[filtered ? 'remove' : 'add'](cls);
},
}));
}
function scriptFromLanguage(language) {
if (language) language = language.toLowerCase(); else return;
const scripts = {
Latn: [
'eng', 'deu', 'spa', 'fra', 'ita', 'swe', 'nor', 'fin', 'por', 'nld', 'pol', 'ces', 'slk', 'slv', 'hrv',
'srp', 'hun', 'tur', 'dan', 'ltz', 'ron', 'est', 'lav', 'isl', 'lat', 'cat', 'gsw', 'fil', 'eus', 'afr',
'lit', 'cym', 'glg', 'bre', 'oci', 'haw', 'gla', 'nob', 'mri', 'zul', 'ast', 'swa', 'som', 'gle',
],
Hant: ['zho'], Jpan: ['jpn'], Kore: ['kor'], Thai: ['tha'],
Cyrl: ['rus', 'bul', 'mkd', 'ukr', 'bos', 'bel', 'mon'],
Deva: ['hin', 'mar', 'san'],
Arab: ['ara', 'ind', 'fas', 'urd', 'msa', 'kaz', 'tuk', 'uzb'],
Hebr: ['heb', 'yid'], Grek: ['grk', 'gre', 'ell'],
Armn: ['hye'], Guru: ['pan'], Taml: ['tam'], Hani: ['vie'], Telu: ['tel'], Mymr: ['mya'],
Mlym: ['mal'], Beng: ['asm'], Geor: ['kat'],
};
for (let script in scripts) if (scripts[script].includes(language)) return script;
}
function frequencyAnalysis(literals, string) {
if (!literals || typeof literals != 'object') throw 'Invalid argument';
if (typeof string == 'string') for (let index = 0; index < string.length; ++index) {
const charCode = string.charCodeAt(index);
if (charCode < 0x20 || charCode == 0x7F) continue;
if (charCode in literals) ++literals[charCode]; else literals[charCode] = 1;
}
}
function detectAlphabet(literals, charSets) {
if (!literals || typeof literals != 'object' || !charSets || typeof charSets != 'object')
throw 'Invalid argument';
const charCodes = Object.keys(literals).map(key => parseInt(key))
if (charSets) for (let key in charSets) {
const charSet = Array.prototype.concat.apply(range(0x20, 0x40).concat(range(0x50, 0x60),
range(0x7B, 0x7E), range(0xA0, 0xBF), 0xD7, 0xF7), charSets[key]);
if (charCodes.every(charCode => charSet.includes(charCode))) return key;
} else throw 'Invalid argument';
}
function parseLanguages(siteName, ignoreLanguages = false) {
let matches = /^(.+?)\s+\(([^\(\)]+)\)$/.exec(siteName);
if (matches != null) {
matches = matches.slice(1);
if (ignoreLanguages) return matches;
const scripts = matches.map(function(namePart, index) {
const literals = { };
frequencyAnalysis(literals, namePart);
return detectScript(literals) || scriptFromLanguage(detectLanguage(literals));
});
if (scripts.every((script, index, scripts) => script && scripts.indexOf(script) == index)
|| scripts.includes('Latn') && scripts.some(script => script != 'Latn')) return matches;
}
return siteName ? [siteName] : [ ];
}
function flashElement(elem) {
if (elem instanceof Element) return elem.animate([
{ offset: 0.0, opacity: 1 },
{ offset: 0.4, opacity: 1 },
{ offset: 0.5, opacity: 0.1 },
{ offset: 0.9, opacity: 0.1 },
], { duration: 600, iterations: Infinity });
}
const torrentId = getTorrentId(tr);
if (!(torrentId > 0)) continue; // assertion failed
let edition = /\b(?:edition_(\d+))\b/.exec(tr.className), editionInfo = tr, torrentDetails = tr;
while (torrentDetails != null && !torrentDetails.classList.contains('torrentdetails'))
torrentDetails = torrentDetails.nextElementSibling;
if (torrentDetails == null) continue; // assertion failed
const linkBox = torrentDetails.querySelector('div.linkbox');
if (linkBox == null) continue;
edition = edition != null ? parseInt(edition[1]) : undefined;
while (editionInfo != null && !editionInfo.classList.contains('edition'))
editionInfo = editionInfo.previousElementSibling;
if (editionInfo != null) editionInfo = editionInfo.querySelector('td.edition_info > strong');
const uniqueValues = ((val, ndx, arr) => val && arr.indexOf(val) == ndx);
const getFormatDescriptions = format => format ? (format.descriptions || [ ])
.concat((format.text || '').split(',').map(descriptor => descriptor.trim()).filter(Boolean)) : [ ];
const theadStyle = 'padding: 4pt; background-color: #AAA4;'
const noLinkDecoration = 'background: none !important; padding: 0 !important;';
const linkHTML = (url, caption, cls) => `<a href="${url}" target="_blank" ${cls ? 'class="' + cls + '" ' : ''}style="${noLinkDecoration}">${caption}</a>`;
// SPAs and SPLs
const mb = makeConst({
spa: {
VA: '89ad4ac3-39f7-470e-963a-56509c546377',
noArtist: 'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
unknown: '125ec42a-7229-4250-afc5-e057484327fe',
anonymous: 'f731ccc4-e22a-43af-a747-64213329e088',
traditional: '9be7f096-97ec-4615-8957-8d40b5dcbc41',
dialogue: '314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
data: '33cf029c-63b0-41a0-9855-be2a3665fb3b',
disney: '66ea0139-149f-4a0c-8fbf-5ea9ec4a6e49',
theatre: 'a0ef7e1d-44ff-4039-9435-7d5fefdeecc9',
churchChimes: '90068d37-bae7-4292-be4a-704c145bd616',
languageInstruction: '80a8851f-444c-4539-892b-ad2a49292aa9',
},
spl: {
noLabel: '157afde4-4bf5-4039-8ad2-5a15acc85176',
unknown: '46caaa9e-3e26-49b5-827c-64ccc73c1b07',
},
});
const stripDiscogsNameVersion = name => name && name.replace(/\s+\(\d+\)$/, '');
const creditedName = entry => entry && (entry.anv || stripDiscogsNameVersion(entry.name));
const discogsEntity = entity => entity && (({
'release-group': 'master',
'series': 'label',
'place': 'label',
}[entity.toLowerCase()]) || entity);
const amEntity = entity => entity && (({
'release': 'album/release',
'release-group': 'album',
'label': 'artist',
}[entity.toLowerCase()]) || entity);
const fmtJoinPhrase = (joinPhrase = '&') => [
[/^\s+(?=[\,\;])/, ''],
[/\b(\w+)\b/g, (...m) => m[1] == m[1].toUpperCase() ? m[1] : m[1].toLowerCase()],
[/\s+(?:feat(?:uring)?|f(?:\/|\.\/?|t\.?\/?)|\/ft?\.?)\s+/ig, ' feat. '],
[/\s+(?:w(?:\/|\.\/?)|\/w\.?)\s+/ig, ' with '],
].reduce((phrase, subst) => phrase.replace(...subst), ' ' + joinPhrase.trim() + ' ');
const rxEditionSplitter = /[\/\|\•\;\,]+/;
const dashUnifier = str => str && str.replace(/[\‐\−\—\–]/g, '-');
const isCD = medium => medium && /^(?:(?:SHM-|HD|HQ|DTS |Enhanced |Blu-spec |Copy Control |Minimax |Mixed Mode )?CD|CD-R|(?:8cm )?CD(?:\+G)?|Hybrid SACD(?: \(CD layer\))?|DualDisc(?: \(CD side\))?)$/.test(medium.format);
const sameMedia = release => release.media.every(medium => !medium.format) ?
release.media : release.media.filter(isCD);
const sameBarcodes = (...barcodes) => barcodes.length > 0
&& barcodes.every((barcode, ndx, arr) => parseInt(cmpNorm(barcode)) == parseInt(cmpNorm(arr[0])));
const discogsCountryToIso3166Mapper = discogsCountry => ({
'US': ['US'], 'UK': ['GB'], 'Germany': ['DE'], 'France': ['FR'], 'Japan': ['JP'], 'Italy': ['IT'],
'Europe': ['XE'], 'Canada': ['CA'], 'Netherlands': ['NL'], 'Spain': ['ES'], 'Australia': ['AU'],
'Russia': ['RU'], 'Sweden': ['SE'], 'Brazil': ['BR'], 'Belgium': ['BE'], 'Greece': ['GR'], 'USSR': ['SU'],
'Poland': ['PL'], 'Mexico': ['MX'], 'Finland': ['FI'], 'Jamaica': ['JM'], 'Switzerland': ['CH'],
'Denmark': ['DK'], 'Argentina': ['AR'], 'Portugal': ['PT'], 'Norway': ['NO'], 'Austria': ['AT'],
'UK & Europe': ['GB', 'XE'], 'New Zealand': ['NZ'], 'Romania': ['RO'], 'Cyprus': ['CY'],
'South Africa': ['ZA'], 'Yugoslavia': ['YU'], 'Hungary': ['HU'], 'Colombia': ['CO'], 'Malaysia': ['MY'],
'USA & Canada': ['US', 'CA'], 'Ukraine': ['UA'], 'Turkey': ['TR'], 'India': ['IN'], 'Indonesia': ['ID'],
'Czech Republic': ['CZ'], 'Czechoslovakia': ['XC'], 'Venezuela': ['VE'], 'Ireland': ['IE'],
'Taiwan': ['TW'], 'Chile': ['CL'], 'Peru': ['PE'], 'South Korea': ['KR'], 'Worldwide': ['XW'],
'Israel': ['IL'], 'Bulgaria': ['BG'], 'Thailand': ['TH'], 'Scandinavia': ['SE', 'NO', 'FI'],
'German Democratic Republic (GDR)': ['XG'], 'China': ['CN'], 'Croatia': ['HR'], 'Hong Kong': ['HK'],
'Philippines': ['PH'], 'Serbia': ['RS'], 'Ecuador': ['EC'], 'Lithuania': ['LT'], 'East Timor': ['TL'],
'UK, Europe & US': ['GB', 'XE', 'US'], 'USA & Europe': ['US', 'XE'], 'Dutch East Indies': ['ID'],
'Germany, Austria, & Switzerland': ['DE', 'AT', 'CH'], 'Singapore': ['SG'], 'Slovenia': ['SI'],
'Slovakia': ['SK'], 'Uruguay': ['UY'], 'Australasia': ['AU'], 'Iceland': ['IS'], 'Bolivia': ['BO'],
'UK & Ireland': ['GB', 'IE'], 'Nigeria': ['NG'], 'Estonia': ['EE'], 'Egypt': ['EG'], 'Cuba': ['CU'],
'USA, Canada & Europe': ['US', 'CA', 'XE'], 'Benelux': ['BE', 'NL', 'LU'], 'Panama': ['PA'],
'UK & US': ['GB', 'US'], 'Pakistan': ['PK'], 'Lebanon': ['LB'], 'Costa Rica': ['CR'], 'Latvia': ['LV'],
'Puerto Rico': ['PR'], 'Kenya': ['KE'], 'Iran': ['IR'], 'Belarus': ['BY'], 'Morocco': ['MA'],
'Guatemala': ['GT'], 'Saudi Arabia': ['SA'], 'Trinidad & Tobago': ['TT'], 'Barbados': ['BB'],
'USA, Canada & UK': ['US', 'CA', 'GB'], 'Luxembourg': ['LU'], 'Czech Republic & Slovakia': ['CZ', 'SK'],
'Bosnia & Herzegovina': ['BA'], 'Macedonia': ['MK'], 'Madagascar': ['MG'], 'Ghana': ['GH'], 'Iraq': ['IQ'],
'Zimbabwe': ['ZW'], 'El Salvador': ['SV'], 'North America (inc Mexico)': ['US', 'CA', 'MX'],
'Algeria': ['DZ'], 'Singapore, Malaysia & Hong Kong': ['SG', 'MY', 'HK'], 'Dominican Republic': ['DO'],
'France & Benelux': ['FR', 'BE', 'NL', 'LU'], 'Ivory Coast': ['CI'], 'Tunisia': ['TN'], 'Kuwait': ['KW'],
'Reunion': ['RE'], 'Angola': ['AO'], 'Serbia and Montenegro': ['RS', 'ME'], 'Georgia': ['GE'],
'United Arab Emirates': ['AE'], 'Congo, Democratic Republic of the': ['CD'], 'Mauritius': ['MU'],
'Germany & Switzerland': ['DE', 'CH'], 'Malta': ['MT'], 'Mozambique': ['MZ'], 'Guadeloupe': ['GP'],
'Australia & New Zealand': ['AU', 'NZ'], 'Azerbaijan': ['AZ'], 'Zambia': ['ZM'], 'Kazakhstan': ['KZ'],
'Nicaragua': ['NI'], 'Syria': ['SY'], 'Senegal': ['SN'], 'Paraguay': ['PY'], 'Wake Island': ['MH'],
'UK & France': ['GB', 'FR'], 'Vietnam': ['VN'], 'UK, Europe & Japan': ['GB', 'XE', 'JP'],
'Bahamas, The': ['BS'], 'Ethiopia': ['ET'], 'Suriname': ['SR'], 'Haiti': ['HT'], 'South America': ['ZA'],
'Singapore & Malaysia': ['SG', 'MY'], 'Moldova, Republic of': ['MD'], 'Faroe Islands': ['FO'],
'Cameroon': ['CM'], 'South Vietnam': ['VN'], 'Uzbekistan': ['UZ'], 'Albania': ['AL'], 'Honduras': ['HN'],
'Martinique': ['MQ'], 'Benin': ['BJ'], 'Sri Lanka': ['LK'], 'Andorra': ['AD'], 'Liechtenstein': ['LI'],
'Curaçao': ['CW'], 'Mali': ['ML'], 'Guinea': ['GN'], 'Congo, Republic of the': ['CG'], 'Sudan': ['SD'],
'Mongolia': ['MN'], 'Nepal': ['NP'], 'French Polynesia': ['PF'], 'Greenland': ['GL'], 'Uganda': ['UG'],
'Bohemia': ['CZ'], 'Bangladesh': ['BD'], 'Armenia': ['AM'], 'North Korea': ['KP'], 'Bermuda': ['BM'],
'Seychelles': ['SC'], 'Cambodia': ['KH'], 'Guyana': ['GY'], 'Tanzania': ['TZ'], 'Bahrain': ['BH'],
'Jordan': ['JO'], 'Libya': ['LY'], 'Montenegro': ['ME'], 'Gabon': ['GA'], 'Togo': ['TG'], 'Yemen': ['YE'],
'Afghanistan': ['AF'], 'Cayman Islands': ['KY'], 'Monaco': ['MC'], 'Papua New Guinea': ['PG'],
'Belize': ['BZ'], 'Fiji': ['FJ'], 'UK & Germany': ['UK', 'DE'], 'New Caledonia': ['NC'], 'Qatar': ['QA'],
'Protectorate of Bohemia and Moravia': ['CZ' /*'XP'*/], 'Saint Helena' : ['SH'], 'Laos': ['LA'], 'Dahomey': ['BJ'],
'UK, Europe & Israel': ['GB', 'XE', 'IL'], 'French Guiana': ['GF'], 'Aruba': ['AW'], 'Dominica': ['DM'],
'San Marino': ['SM'], 'Kyrgyzstan': ['KG'], 'Upper Volta': ['BF'], 'Burkina Faso': ['BF'], 'Oman': ['OM'],
'Turkmenistan': ['TM'], 'Namibia': ['NA'], 'Sierra Leone': ['SL'], 'Marshall Islands': ['MH'],
'Guernsey': ['GG'], 'Jersey': ['JE'], 'Guam': ['GU'], 'Central African Republic': ['CF'], 'Tonga': ['TO'],
'Eritrea': ['ER'], 'Saint Kitts and Nevis': ['KN'], 'Grenada': ['GD'], 'Somalia': ['SO'], 'Malawi': ['MW'],
'Liberia': ['LR'], 'Sint Maarten': ['SX'], 'Saint Lucia': ['LC'], 'Lesotho': ['LS'], 'Maldives': ['MV'],
'Saint Vincent and the Grenadines': ['VC'], 'Guinea-Bissau': ['GW'], 'Botswana': ['BW'], 'Palau': ['PW'],
'Comoros': ['KM'], 'Gibraltar': ['GI'], 'Cook Islands': ['CK'], 'Kosovo': ['XK'], 'Bhutan': ['BT'],
'Gulf Cooperation Council': ['BH', 'KW', 'OM', 'QA', 'SA', 'AE'], 'Niger': ['NE'], 'Mauritania': ['MR'],
'Anguilla': ['AI'], 'Sao Tome and Principe': ['ST'], 'Djibouti': ['DJ'], 'Mayotte': ['YT'],
'Montserrat': ['MS'], 'Vanuatu': ['VU'], 'Norfolk Island': ['NF'], 'Gaza Strip': ['PS'], 'Macau': ['MO'],
'Solomon Islands': ['SB'], 'Turks and Caicos Islands': ['TC'], 'Northern Mariana Islands': ['MP'],
'Equatorial Guinea': ['GQ'], 'American Samoa': ['AS'], 'Chad': ['TD'], 'Falkland Islands': ['FK'],
'Antarctica': ['AQ'], 'Nauru': ['NR'], 'Niue': ['NU'], 'Saint Pierre and Miquelon': ['PM'],
'Tokelau': ['TK'], 'Tuvalu': ['TV'], 'Wallis and Futuna': ['WF'], 'Korea': ['KR'], 'Abkhazia': ['GE'],
'Antigua & Barbuda': ['AG'], 'Austria-Hungary': ['AT', 'HU'], 'British Virgin Islands': ['VG'],
'Brunei': ['BN'], 'Burma': ['MM'], 'Cape Verde': ['CV'], 'Virgin Islands': ['VI'], 'Tibet' : ['CN'],
'Vatican City': ['VA'], 'Swaziland': ['SZ'], 'Southern Sudan': ['SS'], 'Palestine': ['PS'],
'Singapore, Malaysia, Hong Kong & Thailand': ['SG', 'MY', 'HK', 'TH'], 'Pitcairn Islands': ['PN'],
'Micronesia, Federated States of': ['FM'], 'Man, Isle of': ['IM'], 'Zanzibar': ['TZ'], 'Burundi' : ['BI'],
'Korea (pre-1945)': ['KR'], 'Hong Kong & Thailand': ['HK', 'TH'], 'Gambia, The': ['GM'], 'Zaire': ['ZR'],
'South Georgia and the South Sandwich Islands' : ['GS'], 'Cocos (Keeling) Islands' : ['CC'],
'Kiribati' : ['KI'], 'Christmas Island' : ['CX'], 'French Southern & Antarctic Lands' : ['TF'],
'British Indian Ocean Territory' : ['IO'], 'Western Sahara': ['EH'], 'Rhodesia': ['ZW'], 'Samoa': ['WS'],
'Southern Rhodesia': ['ZW'], 'West Bank': ['PS'], 'Belgian Congo': ['CD'], 'Ottoman Empire': ['TR'],
'Netherlands Antilles': ['AW', 'BQ', 'CW', 'BQ', 'SX'], 'Tajikistan': ['TJ'], 'Rwanda': ['RW'],
'Indochina': ['KH', 'MY', 'MM', 'TH', 'VN', ' LA'], 'South West Africa': ['NA'],
'Russia & CIS': ['RU', 'AM', 'AZ', 'BY', 'KZ', 'KG', 'MD', 'TJ', 'UZ']/*.concat('TM', 'UA')*/,
'Central America': ['BZ', 'CR', 'SV', 'GT', 'HN', 'NI', 'PA'],
'South East Asia': ['BN', 'KH', 'TL', 'ID', 'LA', 'MY', 'MM', 'PH', 'SG', 'TH', 'VN'],
'Middle East': ['BH', 'CY', 'EG', 'IR', 'IQ', 'IL', 'JO', 'KW', 'LB', 'OM', 'PS', 'QA', 'SA', 'SY', 'TR', 'AE', 'YE'],
'Asia': ['AF', 'AM', 'AZ', 'BH', 'BD', 'BT', 'BN', 'KH', 'CN', 'CY', 'TL', 'EG', 'GE', 'IN', 'ID', 'IR', 'IQ', 'IL', 'JP', 'JO', 'KZ', 'KW', 'KG', 'LA', 'LB', 'MY', 'MV', 'MN', 'MM', 'NP', 'KP', 'OM', 'PK', 'PS', 'PH', 'QA', 'RU', 'SA', 'SG', 'KR', 'LK', 'SY', 'TW', 'TJ', 'TH', 'TR', 'TM', 'AE', 'UZ', 'VN', 'YE'],
'Africa': ['DZ', 'EG', 'LY', 'MA', 'TN', 'EH', 'BI', 'KM', 'DJ', 'ER', 'ET', 'TF', 'KE', 'MG', 'MW', 'MU', 'YT', 'MZ', 'RE', 'RW', 'SC', 'SO', 'SS', 'SD', 'TZ', 'UG', 'ZM', 'ZW', 'AO', 'CM', 'CF', 'TD', 'CG', 'CD', 'GQ', 'GA', 'ST', 'BW', 'SZ', 'LS', 'NA', 'ZA', 'BJ', 'BF', 'CV', 'GM', 'GH', 'GN', 'GW', 'CI', 'LR', 'ML', 'MR', 'NE', 'NG', 'SH', 'SN', 'SL', 'TG'],
'North & South America': ['AI', 'AG', 'AW', 'BS', 'BB', 'BZ', 'BM', 'BQ', 'VG', 'CA', 'KY', 'CR', 'CU', 'CW', 'DM', 'DO', 'SV', 'GL', 'GD', 'GP', 'GT', 'HT', 'HN', 'JM', 'MQ', 'MX', 'MS', 'NI', 'VE', 'PA', 'PR', 'BL', 'KN', 'LC', 'MF', 'PM', 'VC', 'SX', 'TT', 'TC', 'US', 'VI', 'AR', 'BO', 'BV', 'BR', 'CL', 'CO', 'EC', 'FK', 'GF', 'GY', 'PY', 'PE', 'GS', 'SR', 'UY'],
'South Pacific': ['AU', 'CK', 'FJ', 'KI', 'MH', 'FM', 'NR', 'NZ', 'NU', 'PW', 'PG', 'WS', 'SB', 'TO', 'TV', 'VU'],
'Unknown': [undefined],
}[discogsCountry]) || [discogsCountry || undefined];
const iso3166ToFlagCodes = langCodes => langCodes && Array.prototype.concat.apply([ ], langCodes.map(langCode =>
langCode && (({ XC: ['cz', 'sk'], XP: 'cz' }[langCode.toUpperCase()]) || langCode.toLowerCase())));
const iso3166ToCountryShort = { XE: 'EU', GB: 'UK', XC: 'CS' };
const range = (from, to) => Array.from(Array(to + 1 - from), (_, index) => from + index);
const detectLanguage = literals => detectAlphabet(literals, {
eng: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC], fra: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xAB, range(0xB2, 0xB3), 0xBB, 0xC0, 0xC2, range(0xC6, 0xCB), range(0xCE, 0xCF), 0xD4, 0xD9, range(0xDB, 0xDC), 0xE0, 0xE2, range(0xE6, 0xEB), range(0xEE, 0xEF), 0xF4, 0xF9, range(0xFB, 0xFC), 0xFF, range(0x152, 0x153), 0x178, 0x2B3, 0x2E2, range(0x1D48, 0x1D49), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2019, range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, range(0x202F, 0x2030), 0x20AC, 0x2212],
jpn: [range(0x20, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xB6, range(0x2010, 0x2011), range(0x2014, 0x2016), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), range(0x2025, 0x2026), 0x2030, range(0x2032, 0x2033), 0x203B, 0x203E, 0x20AC, range(0x3001, 0x3003), 0x3005, range(0x3008, 0x3011), range(0x3014, 0x3015), 0x301C, range(0x3041, 0x3093), range(0x309D, 0x309E), range(0x30A1, 0x30F6), range(0x30FB, 0x30FE), range(0x4E00, 0x4E01), 0x4E03, range(0x4E07, 0x4E0B), range(0x4E0D, 0x4E0E), 0x4E14, 0x4E16, range(0x4E18, 0x4E19), 0x4E21, 0x4E26, 0x4E2D, 0x4E32, range(0x4E38, 0x4E39), range(0x4E3B, 0x4E3C), 0x4E45, 0x4E4F, 0x4E57, 0x4E59, range(0x4E5D, 0x4E5E), 0x4E71, 0x4E73, 0x4E7E, 0x4E80, 0x4E86, range(0x4E88, 0x4E89), range(0x4E8B, 0x4E8C), 0x4E92, range(0x4E94, 0x4E95), 0x4E9C, 0x4EA1, 0x4EA4, range(0x4EAB, 0x4EAD), 0x4EBA, 0x4EC1, range(0x4ECA, 0x4ECB), 0x4ECF, range(0x4ED5, 0x4ED6), range(0x4ED8, 0x4ED9), range(0x4EE3, 0x4EE5), 0x4EEE, 0x4EF0, 0x4EF2, 0x4EF6, 0x4EFB, 0x4F01, range(0x4F0E, 0x4F11), 0x4F1A, 0x4F1D, 0x4F2F, 0x4F34, 0x4F38, 0x4F3A, 0x4F3C, 0x4F46, range(0x4F4D, 0x4F50), 0x4F53, 0x4F55, 0x4F59, 0x4F5C, 0x4F73, 0x4F75, 0x4F7F, 0x4F8B, 0x4F8D, 0x4F9B, 0x4F9D, 0x4FA1, range(0x4FAE, 0x4FAF), range(0x4FB5, 0x4FB6), 0x4FBF, range(0x4FC2, 0x4FC3), 0x4FCA, 0x4FD7, 0x4FDD, 0x4FE1, 0x4FEE, 0x4FF3, 0x4FF5, 0x4FF8, 0x4FFA, 0x5009, 0x500B, 0x500D, 0x5012, 0x5019, 0x501F, range(0x5023, 0x5024), 0x502B, 0x5039, 0x5049, 0x504F, 0x505C, 0x5065, range(0x5074, 0x5076), 0x507D, 0x508D, 0x5091, range(0x5098, 0x5099), 0x50AC, 0x50B2, 0x50B5, 0x50B7, 0x50BE, 0x50C5, 0x50CD, 0x50CF, 0x50D5, 0x50DA, 0x50E7, 0x5100, 0x5104, 0x5112, 0x511F, 0x512A, range(0x5143, 0x5146), range(0x5148, 0x5149), 0x514B, 0x514D, 0x5150, 0x515A, 0x5165, 0x5168, range(0x516B, 0x516D), 0x5171, 0x5175, range(0x5177, 0x5178), 0x517C, range(0x5185, 0x5186), 0x518A, 0x518D, 0x5192, 0x5197, 0x5199, 0x51A0, 0x51A5, 0x51AC, range(0x51B6, 0x51B7), 0x51C4, 0x51C6, 0x51CD, 0x51DD, 0x51E1, 0x51E6, 0x51F6, range(0x51F8, 0x51FA), 0x5200, 0x5203, range(0x5206, 0x5208), 0x520A, 0x5211, 0x5217, 0x521D, range(0x5224, 0x5225), 0x5229, 0x5230, range(0x5236, 0x523B), 0x5247, 0x524A, 0x524D, 0x5256, 0x525B, range(0x5263, 0x5265), range(0x526F, 0x5270), 0x5272, 0x5275, 0x5287, 0x529B, range(0x529F, 0x52A0), 0x52A3, range(0x52A9, 0x52AA), 0x52B1, 0x52B4, 0x52B9, 0x52BE, 0x52C3, 0x52C5, 0x52C7, 0x52C9, 0x52D5, range(0x52D8, 0x52D9), 0x52DD, 0x52DF, 0x52E2, 0x52E4, 0x52E7, 0x52F2, 0x52FE, 0x5302, 0x5305, range(0x5316, 0x5317), 0x5320, range(0x5339, 0x533B), 0x533F, 0x5341, 0x5343, range(0x5347, 0x5348), 0x534A, range(0x5351, 0x5354), range(0x5357, 0x5358), 0x535A, 0x5360, range(0x5370, 0x5371), range(0x5373, 0x5375), 0x5378, 0x5384, 0x5398, 0x539A, 0x539F, 0x53B3, 0x53BB, 0x53C2, 0x53C8, range(0x53CA, 0x53CE), 0x53D4, range(0x53D6, 0x53D7), 0x53D9, range(0x53E3, 0x53E5), range(0x53EB, 0x53EC), range(0x53EF, 0x53F3), range(0x53F7, 0x53F8), 0x5404, range(0x5408, 0x5409), range(0x540C, 0x5411), 0x541B, 0x541F, 0x5426, 0x542B, range(0x5438, 0x5439), 0x5442, range(0x5448, 0x544A), 0x5468, 0x546A, 0x5473, range(0x547C, 0x547D), 0x548C, 0x54B2, 0x54BD, range(0x54C0, 0x54C1), 0x54E1, 0x54F2, 0x54FA, 0x5504, range(0x5506, 0x5507), 0x5510, 0x552F, 0x5531, 0x553E, 0x5546, 0x554F, 0x5553, 0x5584, 0x5589, 0x559A, range(0x559C, 0x559D), range(0x55A9, 0x55AB), 0x55B6, 0x55C5, 0x55E3, 0x5606, range(0x5631, 0x5632), 0x5668, 0x5674, 0x5687, range(0x56DA, 0x56DB), 0x56DE, 0x56E0, 0x56E3, 0x56F0, range(0x56F2, 0x56F3), 0x56FA, 0x56FD, 0x570F, 0x5712, 0x571F, range(0x5727, 0x5728), 0x5730, 0x5742, 0x5747, 0x574A, 0x5751, 0x576A, 0x5782, 0x578B, 0x57A3, 0x57CB, 0x57CE, 0x57DF, 0x57F7, range(0x57F9, 0x57FA), 0x57FC, 0x5800, 0x5802, range(0x5805, 0x5806), 0x5815, 0x5824, 0x582A, 0x5831, 0x5834, range(0x5840, 0x5841), 0x584A, 0x5851, 0x5854, 0x5857, 0x585A, 0x585E, 0x5869, 0x586B, 0x587E, 0x5883, 0x5893, 0x5897, 0x589C, 0x58A8, 0x58B3, 0x58BE, 0x58C1, 0x58C7, 0x58CA, 0x58CC, 0x58EB, 0x58EE, range(0x58F0, 0x58F2), 0x5909, 0x590F, range(0x5915, 0x5916), 0x591A, 0x591C, 0x5922, 0x5927, range(0x5929, 0x592B), 0x592E, 0x5931, range(0x5947, 0x5949), 0x594F, 0x5951, 0x5954, 0x5965, 0x5968, 0x596A, 0x596E, range(0x5973, 0x5974), 0x597D, range(0x5982, 0x5984), 0x598A, 0x5996, 0x5999, 0x59A5, 0x59A8, 0x59AC, 0x59B9, 0x59BB, 0x59C9, 0x59CB, range(0x59D3, 0x59D4), 0x59EB, 0x59FB, 0x59FF, 0x5A01, 0x5A18, 0x5A20, 0x5A2F, 0x5A46, 0x5A5A, 0x5A66, 0x5A7F, 0x5A92, 0x5A9B, 0x5AC1, 0x5AC9, 0x5ACC, 0x5AE1, 0x5B22, 0x5B50, 0x5B54, range(0x5B57, 0x5B58), 0x5B5D, range(0x5B63, 0x5B64), 0x5B66, 0x5B6B, 0x5B85, range(0x5B87, 0x5B89), 0x5B8C, range(0x5B97, 0x5B9D), 0x5B9F, range(0x5BA2, 0x5BA4), 0x5BAE, 0x5BB0, range(0x5BB3, 0x5BB6), 0x5BB9, 0x5BBF, 0x5BC2, 0x5BC4, 0x5BC6, 0x5BCC, 0x5BD2, 0x5BDB, 0x5BDD, 0x5BDF, 0x5BE1, 0x5BE7, 0x5BE9, 0x5BEE, 0x5BF8, 0x5BFA, range(0x5BFE, 0x5BFF), range(0x5C01, 0x5C02), 0x5C04, 0x5C06, range(0x5C09, 0x5C0B), range(0x5C0E, 0x5C0F), 0x5C11, 0x5C1A, 0x5C31, range(0x5C3A, 0x5C40), 0x5C45, 0x5C48, range(0x5C4A, 0x5C4B), 0x5C55, 0x5C5E, range(0x5C64, 0x5C65), 0x5C6F, 0x5C71, 0x5C90, 0x5CA1, 0x5CA9, 0x5CAC, 0x5CB3, 0x5CB8, range(0x5CE0, 0x5CE1), 0x5CF0, 0x5CF6, 0x5D07, 0x5D0E, 0x5D16, 0x5D29, 0x5D50, range(0x5DDD, 0x5DDE), 0x5DE1, 0x5DE3, range(0x5DE5, 0x5DE8), 0x5DEE, 0x5DF1, 0x5DFB, 0x5DFE, range(0x5E02, 0x5E03), 0x5E06, 0x5E0C, 0x5E1D, 0x5E25, 0x5E2B, 0x5E2D, range(0x5E2F, 0x5E30), 0x5E33, 0x5E38, 0x5E3D, 0x5E45, 0x5E55, 0x5E63, range(0x5E72, 0x5E74), range(0x5E78, 0x5E79), range(0x5E7B, 0x5E7E), 0x5E81, 0x5E83, 0x5E8A, 0x5E8F, 0x5E95, 0x5E97, 0x5E9C, range(0x5EA6, 0x5EA7), 0x5EAB, 0x5EAD, range(0x5EB6, 0x5EB8), 0x5EC3, range(0x5EC9, 0x5ECA), range(0x5EF6, 0x5EF7), 0x5EFA, 0x5F01, 0x5F04, 0x5F0A, range(0x5F0F, 0x5F10), range(0x5F13, 0x5F15), 0x5F1F, range(0x5F25, 0x5F27), 0x5F31, 0x5F35, 0x5F37, 0x5F3E, 0x5F53, 0x5F59, 0x5F62, 0x5F69, 0x5F6B, range(0x5F70, 0x5F71), 0x5F79, 0x5F7C, range(0x5F80, 0x5F81), range(0x5F84, 0x5F85), range(0x5F8B, 0x5F8C), 0x5F90, range(0x5F92, 0x5F93), 0x5F97, 0x5FA1, range(0x5FA9, 0x5FAA), 0x5FAE, range(0x5FB3, 0x5FB4), 0x5FB9, 0x5FC3, 0x5FC5, range(0x5FCC, 0x5FCD), range(0x5FD7, 0x5FD9), 0x5FDC, 0x5FE0, 0x5FEB, 0x5FF5, 0x6012, 0x6016, 0x601D, 0x6020, 0x6025, range(0x6027, 0x6028), 0x602A, 0x604B, 0x6050, 0x6052, 0x6063, 0x6065, range(0x6068, 0x6069), 0x606D, 0x606F, 0x6075, 0x6094, range(0x609F, 0x60A0), 0x60A3, 0x60A6, range(0x60A9, 0x60AA), 0x60B2, 0x60BC, 0x60C5, 0x60D1, 0x60DC, range(0x60E7, 0x60E8), 0x60F0, 0x60F3, 0x6101, 0x6109, 0x610F, range(0x611A, 0x611B), 0x611F, 0x6144, 0x6148, range(0x614B, 0x614C), 0x614E, 0x6155, range(0x6162, 0x6163), 0x6168, 0x616E, 0x6170, 0x6176, 0x6182, 0x618E, 0x61A4, 0x61A7, 0x61A9, 0x61AC, 0x61B2, 0x61B6, 0x61BE, 0x61C7, 0x61D0, 0x61F2, 0x61F8, range(0x6210, 0x6212), 0x621A, 0x6226, 0x622F, 0x6234, 0x6238, 0x623B, range(0x623F, 0x6240), 0x6247, 0x6249, 0x624B, 0x624D, 0x6253, 0x6255, 0x6271, 0x6276, 0x6279, range(0x627F, 0x6280), 0x6284, 0x628A, 0x6291, 0x6295, range(0x6297, 0x6298), 0x629C, 0x629E, 0x62AB, 0x62B1, 0x62B5, 0x62B9, range(0x62BC, 0x62BD), 0x62C5, 0x62C9, 0x62CD, 0x62D0, range(0x62D2, 0x62D3), range(0x62D8, 0x62D9), 0x62DB, 0x62DD, range(0x62E0, 0x62E1), range(0x62EC, 0x62ED), 0x62F3, range(0x62F6, 0x62F7), 0x62FE, 0x6301, 0x6307, 0x6311, 0x6319, 0x631F, 0x6328, 0x632B, 0x632F, 0x633F, 0x6349, 0x6355, 0x6357, 0x635C, 0x6368, 0x636E, 0x637B, 0x6383, 0x6388, 0x638C, 0x6392, 0x6398, 0x639B, range(0x63A1, 0x63A2), 0x63A5, range(0x63A7, 0x63A8), 0x63AA, 0x63B2, range(0x63CF, 0x63D0), range(0x63DA, 0x63DB), 0x63E1, 0x63EE, 0x63F4, 0x63FA, 0x640D, range(0x642C, 0x642D), 0x643A, 0x643E, 0x6442, 0x6458, 0x6469, 0x646F, 0x6483, 0x64A4, 0x64AE, 0x64B2, 0x64C1, 0x64CD, 0x64E6, 0x64EC, 0x652F, 0x6539, 0x653B, range(0x653E, 0x653F), 0x6545, 0x654F, 0x6551, 0x6557, 0x6559, range(0x6562, 0x6563), 0x656C, 0x6570, range(0x6574, 0x6575), 0x6577, 0x6587, 0x6589, 0x658E, 0x6591, 0x6597, 0x6599, 0x659C, range(0x65A4, 0x65A5), range(0x65AC, 0x65AD), 0x65B0, 0x65B9, 0x65BD, 0x65C5, 0x65CB, 0x65CF, 0x65D7, 0x65E2, range(0x65E5, 0x65E9), 0x65EC, 0x65FA, range(0x6606, 0x6607), 0x660E, range(0x6613, 0x6614), range(0x661F, 0x6620), 0x6625, range(0x6627, 0x6628), 0x662D, 0x662F, 0x663C, 0x6642, 0x6669, range(0x666E, 0x666F), 0x6674, 0x6676, 0x6681, 0x6687, 0x6691, range(0x6696, 0x6697), 0x66A6, 0x66AB, 0x66AE, 0x66B4, 0x66C7, 0x66D6, 0x66DC, 0x66F2, 0x66F4, range(0x66F8, 0x66F9), 0x66FD, range(0x66FF, 0x6700), range(0x6708, 0x6709), 0x670D, 0x6715, 0x6717, 0x671B, 0x671D, 0x671F, 0x6728, range(0x672A, 0x672D), 0x6731, 0x6734, 0x673A, 0x673D, 0x6749, range(0x6750, 0x6751), 0x675F, 0x6761, 0x6765, 0x676F, 0x6771, range(0x677E, 0x677F), 0x6790, 0x6795, 0x6797, 0x679A, range(0x679C, 0x679D), 0x67A0, 0x67A2, 0x67AF, 0x67B6, 0x67C4, 0x67D0, range(0x67D3, 0x67D4), 0x67F1, 0x67F3, 0x67F5, 0x67FB, 0x67FF, range(0x6803, 0x6804), 0x6813, 0x6821, 0x682A, range(0x6838, 0x6839), range(0x683C, 0x683D), 0x6841, 0x6843, 0x6848, 0x6851, 0x685C, 0x685F, 0x6885, 0x6897, 0x68A8, 0x68B0, 0x68C4, 0x68CB, 0x68D2, 0x68DA, 0x68DF, 0x68EE, 0x68FA, 0x6905, range(0x690D, 0x690E), 0x691C, 0x696D, 0x6975, 0x6977, range(0x697C, 0x697D), 0x6982, 0x69CB, 0x69D8, 0x69FD, 0x6A19, 0x6A21, range(0x6A29, 0x6A2A), 0x6A39, 0x6A4B, 0x6A5F, 0x6B04, range(0x6B20, 0x6B21), 0x6B27, 0x6B32, 0x6B3A, 0x6B3E, 0x6B4C, 0x6B53, range(0x6B62, 0x6B63), 0x6B66, 0x6B69, 0x6B6F, range(0x6B73, 0x6B74), 0x6B7B, range(0x6B89, 0x6B8B), 0x6B96, range(0x6BB4, 0x6BB5), range(0x6BBA, 0x6BBB), range(0x6BBF, 0x6BC0), range(0x6BCD, 0x6BCE), 0x6BD2, 0x6BD4, 0x6BDB, 0x6C0F, 0x6C11, 0x6C17, 0x6C34, range(0x6C37, 0x6C38), 0x6C3E, range(0x6C41, 0x6C42), 0x6C4E, 0x6C57, 0x6C5A, range(0x6C5F, 0x6C60), 0x6C70, 0x6C7A, 0x6C7D, 0x6C83, 0x6C88, 0x6C96, 0x6C99, range(0x6CA1, 0x6CA2), 0x6CB3, range(0x6CB8, 0x6CB9), range(0x6CBB, 0x6CBC), 0x6CBF, 0x6CC1, range(0x6CC9, 0x6CCA), 0x6CCC, 0x6CD5, range(0x6CE1, 0x6CE3), 0x6CE5, 0x6CE8, 0x6CF0, 0x6CF3, 0x6D0B, 0x6D17, 0x6D1E, 0x6D25, 0x6D2A, 0x6D3B, 0x6D3E, 0x6D41, range(0x6D44, 0x6D45), 0x6D5C, 0x6D66, 0x6D6A, 0x6D6E, 0x6D74, range(0x6D77, 0x6D78), 0x6D88, 0x6D99, 0x6DAF, 0x6DB2, 0x6DBC, 0x6DD1, 0x6DE1, 0x6DEB, 0x6DF1, 0x6DF7, 0x6DFB, 0x6E05, range(0x6E07, 0x6E09), 0x6E0B, 0x6E13, 0x6E1B, 0x6E21, 0x6E26, 0x6E29, 0x6E2C, 0x6E2F, 0x6E56, 0x6E67, 0x6E6F, range(0x6E7E, 0x6E80), 0x6E90, 0x6E96, 0x6E9D, 0x6EB6, 0x6EBA, 0x6EC5, 0x6ECB, 0x6ED1, range(0x6EDD, 0x6EDE), 0x6EF4, range(0x6F01, 0x6F02), 0x6F06, 0x6F0F, 0x6F14, 0x6F20, 0x6F22, range(0x6F2B, 0x6F2C), 0x6F38, 0x6F54, 0x6F5C, 0x6F5F, 0x6F64, 0x6F6E, 0x6F70, 0x6F84, range(0x6FC0, 0x6FC1), 0x6FC3, 0x6FEB, 0x6FEF, 0x702C, 0x706B, range(0x706F, 0x7070), 0x707D, range(0x7089, 0x708A), 0x708E, 0x70AD, range(0x70B9, 0x70BA), 0x70C8, 0x7121, 0x7126, 0x7136, 0x713C, 0x714E, 0x7159, 0x7167, 0x7169, 0x716E, 0x718A, 0x719F, 0x71B1, 0x71C3, 0x71E5, 0x7206, 0x722A, range(0x7235, 0x7236), 0x723D, range(0x7247, 0x7248), 0x7259, 0x725B, 0x7267, 0x7269, 0x7272, 0x7279, 0x72A0, 0x72AC, 0x72AF, 0x72B6, 0x72C2, 0x72D9, 0x72E9, range(0x72EC, 0x72ED), 0x731B, 0x731F, 0x732B, 0x732E, 0x7336, 0x733F, 0x7344, 0x7363, 0x7372, 0x7384, 0x7387, 0x7389, 0x738B, 0x73A9, 0x73CD, 0x73E0, 0x73ED, 0x73FE, 0x7403, 0x7406, 0x7434, 0x7460, 0x7483, 0x74A7, 0x74B0, 0x74BD, 0x74E6, 0x74F6, 0x7518, 0x751A, 0x751F, 0x7523, 0x7528, range(0x7530, 0x7533), 0x7537, range(0x753A, 0x753B), 0x754C, 0x754F, 0x7551, 0x7554, 0x7559, range(0x755C, 0x755D), 0x7565, 0x756A, 0x7570, 0x7573, 0x757F, 0x758E, 0x7591, 0x75AB, 0x75B2, 0x75BE, 0x75C5, 0x75C7, 0x75D5, 0x75D8, 0x75DB, 0x75E2, 0x75E9, 0x75F4, 0x760D, 0x7642, 0x7652, 0x7656, range(0x767A, 0x767B), range(0x767D, 0x767E), 0x7684, range(0x7686, 0x7687), 0x76AE, 0x76BF, 0x76C6, 0x76CA, 0x76D7, 0x76DB, 0x76DF, range(0x76E3, 0x76E4), 0x76EE, 0x76F2, 0x76F4, 0x76F8, 0x76FE, 0x7701, 0x7709, range(0x770B, 0x770C), range(0x771F, 0x7720), 0x773A, 0x773C, 0x7740, 0x7761, 0x7763, 0x7766, range(0x77AC, 0x77AD), 0x77B3, 0x77DB, 0x77E2, 0x77E5, 0x77ED, 0x77EF, 0x77F3, 0x7802, range(0x7814, 0x7815), 0x7832, 0x7834, 0x785D, range(0x786B, 0x786C), 0x7881, 0x7891, 0x78BA, 0x78C1, 0x78E8, 0x7901, 0x790E, 0x793A, 0x793C, 0x793E, range(0x7948, 0x7949), 0x7956, range(0x795D, 0x795E), 0x7965, 0x7968, 0x796D, 0x7981, 0x7985, 0x798D, 0x798F, range(0x79C0, 0x79C1), 0x79CB, range(0x79D1, 0x79D2), 0x79D8, 0x79DF, 0x79E9, 0x79F0, 0x79FB, 0x7A0B, 0x7A0E, 0x7A1A, 0x7A2E, 0x7A32, range(0x7A3C, 0x7A3D), range(0x7A3F, 0x7A40), 0x7A42, 0x7A4D, 0x7A4F, 0x7A6B, 0x7A74, 0x7A76, 0x7A7A, 0x7A81, 0x7A83, range(0x7A92, 0x7A93), 0x7A9F, range(0x7AAE, 0x7AAF), 0x7ACB, 0x7ADC, 0x7AE0, 0x7AE5, 0x7AEF, 0x7AF6, 0x7AF9, 0x7B11, 0x7B1B, 0x7B26, 0x7B2C, 0x7B46, 0x7B49, 0x7B4B, 0x7B52, 0x7B54, 0x7B56, 0x7B87, 0x7B8B, 0x7B97, 0x7BA1, 0x7BB1, 0x7BB8, 0x7BC0, 0x7BC4, 0x7BC9, 0x7BE4, 0x7C21, 0x7C3F, 0x7C4D, 0x7C60, 0x7C73, 0x7C89, 0x7C8B, 0x7C92, range(0x7C97, 0x7C98), 0x7C9B, 0x7CA7, 0x7CBE, 0x7CD6, 0x7CE7, 0x7CF8, 0x7CFB, 0x7CFE, 0x7D00, range(0x7D04, 0x7D05), 0x7D0B, 0x7D0D, 0x7D14, range(0x7D19, 0x7D1B), range(0x7D20, 0x7D22), 0x7D2B, range(0x7D2F, 0x7D30), 0x7D33, range(0x7D39, 0x7D3A), 0x7D42, 0x7D44, 0x7D4C, 0x7D50, 0x7D5E, 0x7D61, 0x7D66, 0x7D71, range(0x7D75, 0x7D76), 0x7D79, range(0x7D99, 0x7D9A), 0x7DAD, range(0x7DB1, 0x7DB2), 0x7DBB, 0x7DBF, 0x7DCA, 0x7DCF, range(0x7DD1, 0x7DD2), 0x7DDA, 0x7DE0, range(0x7DE8, 0x7DE9), 0x7DEF, 0x7DF4, 0x7DFB, 0x7E01, 0x7E04, 0x7E1B, 0x7E26, 0x7E2B, 0x7E2E, 0x7E3E, 0x7E41, 0x7E4A, range(0x7E54, 0x7E55), 0x7E6D, 0x7E70, 0x7F36, 0x7F6A, 0x7F6E, 0x7F70, 0x7F72, 0x7F75, 0x7F77, 0x7F85, 0x7F8A, 0x7F8E, 0x7F9E, 0x7FA4, range(0x7FA8, 0x7FA9), 0x7FBD, 0x7FC1, 0x7FCC, 0x7FD2, range(0x7FFB, 0x7FFC), 0x8001, 0x8003, 0x8005, 0x8010, 0x8015, 0x8017, 0x8033, 0x8056, 0x805E, 0x8074, 0x8077, 0x8089, 0x808C, 0x8096, 0x8098, 0x809D, range(0x80A1, 0x80A2), 0x80A5, range(0x80A9, 0x80AA), 0x80AF, 0x80B2, 0x80BA, 0x80C3, 0x80C6, 0x80CC, 0x80CE, 0x80DE, 0x80F4, 0x80F8, 0x80FD, 0x8102, 0x8105, range(0x8107, 0x8108), 0x810A, 0x811A, 0x8131, 0x8133, 0x814E, 0x8150, 0x8155, 0x816B, 0x8170, range(0x8178, 0x817A), 0x819A, range(0x819C, 0x819D), 0x81A8, 0x81B3, 0x81C6, 0x81D3, 0x81E3, 0x81E8, 0x81EA, 0x81ED, range(0x81F3, 0x81F4), 0x81FC, 0x8208, 0x820C, 0x820E, 0x8217, range(0x821E, 0x821F), 0x822A, 0x822C, range(0x8236, 0x8237), 0x8239, 0x8247, 0x8266, 0x826F, 0x8272, 0x8276, 0x828B, 0x829D, 0x82AF, 0x82B1, 0x82B3, 0x82B8, 0x82BD, 0x82D7, 0x82DB, range(0x82E5, 0x82E6), 0x82F1, 0x8302, 0x830E, 0x8328, 0x8336, 0x8349, 0x8352, 0x8358, 0x8377, 0x83CA, 0x83CC, 0x83D3, 0x83DC, 0x83EF, 0x840E, 0x843D, 0x8449, 0x8457, 0x845B, 0x846C, 0x84B8, 0x84C4, 0x84CB, 0x8511, 0x8535, 0x853D, 0x8584, 0x85A6, range(0x85AA, 0x85AC), 0x85CD, 0x85E4, 0x85E9, 0x85FB, 0x864E, 0x8650, 0x865A, 0x865C, 0x865E, 0x866B, 0x8679, 0x868A, 0x8695, 0x86C7, 0x86CD, 0x86EE, 0x8702, 0x871C, 0x878D, 0x8840, 0x8846, 0x884C, 0x8853, 0x8857, 0x885B, 0x885D, 0x8861, 0x8863, 0x8868, 0x8870, 0x8877, 0x888B, 0x8896, 0x88AB, range(0x88C1, 0x88C2), 0x88C5, 0x88CF, 0x88D5, 0x88DC, 0x88F8, range(0x88FD, 0x88FE), 0x8907, 0x8910, 0x8912, 0x895F, 0x8972, 0x897F, 0x8981, range(0x8986, 0x8987), 0x898B, 0x898F, 0x8996, 0x899A, 0x89A7, 0x89AA, 0x89B3, 0x89D2, 0x89E3, 0x89E6, 0x8A00, range(0x8A02, 0x8A03), 0x8A08, 0x8A0E, 0x8A13, range(0x8A17, 0x8A18), 0x8A1F, 0x8A2A, 0x8A2D, 0x8A31, range(0x8A33, 0x8A34), 0x8A3A, 0x8A3C, 0x8A50, range(0x8A54, 0x8A55), 0x8A5E, 0x8A60, 0x8A63, 0x8A66, 0x8A69, 0x8A6E, range(0x8A70, 0x8A73), 0x8A87, 0x8A89, range(0x8A8C, 0x8A8D), 0x8A93, 0x8A95, 0x8A98, 0x8A9E, 0x8AA0, 0x8AA4, range(0x8AAC, 0x8AAD), 0x8AB0, 0x8AB2, 0x8ABF, 0x8AC7, 0x8ACB, 0x8AD6, range(0x8AE6, 0x8AE7), range(0x8AED, 0x8AEE), 0x8AF8, 0x8AFE, range(0x8B00, 0x8B01), 0x8B04, 0x8B0E, 0x8B19, 0x8B1B, 0x8B1D, 0x8B21, 0x8B39, 0x8B58, 0x8B5C, 0x8B66, 0x8B70, 0x8B72, 0x8B77, 0x8C37, 0x8C46, 0x8C4A, 0x8C5A, 0x8C61, 0x8C6A, 0x8C8C, range(0x8C9D, 0x8C9E), range(0x8CA0, 0x8CA2), range(0x8CA7, 0x8CAC), 0x8CAF, 0x8CB4, range(0x8CB7, 0x8CB8), range(0x8CBB, 0x8CBC), range(0x8CBF, 0x8CC0), range(0x8CC2, 0x8CC4), 0x8CC7, 0x8CCA, 0x8CD3, range(0x8CDB, 0x8CDC), 0x8CDE, 0x8CE0, 0x8CE2, 0x8CE6, 0x8CEA, 0x8CED, 0x8CFC, 0x8D08, 0x8D64, 0x8D66, 0x8D70, 0x8D74, 0x8D77, 0x8D85, 0x8D8A, 0x8DA3, 0x8DB3, 0x8DDD, 0x8DE1, 0x8DEF, 0x8DF3, 0x8DF5, 0x8E0A, 0x8E0F, 0x8E2A, 0x8E74, 0x8E8D, 0x8EAB, 0x8ECA, range(0x8ECC, 0x8ECD), 0x8ED2, 0x8EDF, 0x8EE2, 0x8EF8, 0x8EFD, 0x8F03, 0x8F09, 0x8F1D, range(0x8F29, 0x8F2A), 0x8F38, 0x8F44, 0x8F9B, 0x8F9E, 0x8FA3, range(0x8FB1, 0x8FB2), 0x8FBA, 0x8FBC, 0x8FC5, 0x8FCE, 0x8FD1, 0x8FD4, 0x8FEB, 0x8FED, 0x8FF0, 0x8FF7, 0x8FFD, range(0x9000, 0x9001), 0x9003, 0x9006, range(0x900F, 0x9010), range(0x9013, 0x9014), 0x901A, 0x901D, range(0x901F, 0x9020), 0x9023, 0x902E, range(0x9031, 0x9032), 0x9038, 0x9042, 0x9045, 0x9047, range(0x904A, 0x904B), range(0x904D, 0x904E), range(0x9053, 0x9055), 0x905C, range(0x9060, 0x9061), 0x9063, 0x9069, range(0x906D, 0x906E), 0x9075, range(0x9077, 0x9078), 0x907A, 0x907F, 0x9084, 0x90A3, 0x90A6, 0x90AA, 0x90B8, 0x90CA, 0x90CE, 0x90E1, 0x90E8, 0x90ED, 0x90F5, 0x90F7, 0x90FD, range(0x914C, 0x914E), 0x9152, 0x9154, 0x9162, 0x916A, 0x916C, 0x9175, range(0x9177, 0x9178), 0x9192, 0x919C, 0x91B8, range(0x91C7, 0x91C8), range(0x91CC, 0x91CF), 0x91D1, range(0x91DC, 0x91DD), 0x91E3, 0x920D, 0x9234, 0x9244, 0x925B, 0x9262, 0x9271, 0x9280, 0x9283, 0x9285, 0x9298, 0x92AD, 0x92ED, 0x92F3, 0x92FC, 0x9320, 0x9326, 0x932C, range(0x932E, 0x932F), 0x9332, 0x934B, 0x935B, 0x9375, 0x938C, 0x9396, 0x93AE, 0x93E1, 0x9418, 0x9451, 0x9577, 0x9580, 0x9589, 0x958B, 0x9591, 0x9593, range(0x95A2, 0x95A3), 0x95A5, 0x95B2, 0x95C7, 0x95D8, 0x961C, 0x962A, 0x9632, 0x963B, 0x9644, 0x964D, 0x9650, 0x965B, range(0x9662, 0x9665), 0x966A, 0x9670, 0x9673, range(0x9675, 0x9676), 0x9678, 0x967A, 0x967D, range(0x9685, 0x9686), 0x968A, range(0x968E, 0x968F), 0x9694, 0x9699, range(0x969B, 0x969C), 0x96A0, 0x96A3, 0x96B7, 0x96BB, range(0x96C4, 0x96C7), 0x96CC, 0x96D1, range(0x96E2, 0x96E3), 0x96E8, 0x96EA, 0x96F0, 0x96F2, range(0x96F6, 0x96F7), 0x96FB, 0x9700, 0x9707, 0x970A, 0x971C, 0x9727, 0x9732, 0x9752, 0x9759, 0x975E, 0x9762, 0x9769, 0x9774, 0x97D3, 0x97F3, 0x97FB, 0x97FF, range(0x9802, 0x9803), range(0x9805, 0x9806), 0x9808, range(0x9810, 0x9813), 0x9818, range(0x982C, 0x982D), range(0x983B, 0x983C), range(0x984C, 0x984E), range(0x9854, 0x9855), 0x9858, 0x985E, 0x9867, 0x98A8, 0x98DB, 0x98DF, 0x98E2, 0x98EF, 0x98F2, range(0x98FC, 0x98FE), 0x9905, 0x990A, 0x990C, 0x9913, 0x9928, 0x9996, 0x9999, 0x99AC, range(0x99C4, 0x99C6), 0x99D0, 0x99D2, 0x9A0E, range(0x9A12, 0x9A13), 0x9A30, 0x9A5A, 0x9AA8, 0x9AB8, 0x9AC4, 0x9AD8, 0x9AEA, 0x9B31, 0x9B3C, 0x9B42, 0x9B45, 0x9B54, 0x9B5A, 0x9BAE, 0x9BE8, 0x9CE5, 0x9CF4, 0x9D8F, 0x9DB4, 0x9E7F, 0x9E93, 0x9E97, 0x9EA6, range(0x9EBA, 0x9EBB), 0x9EC4, 0x9ED2, 0x9ED9, 0x9F13, 0x9F3B, 0x9F62, range(0xFF01, 0xFF03), range(0xFF05, 0xFF0A), range(0xFF0C, 0xFF0F), range(0xFF1A, 0xFF1B), range(0xFF1F, 0xFF20), range(0xFF3B, 0xFF3D), 0xFF3F, 0xFF5B, 0xFF5D, range(0xFF61, 0xFF65)],
spa: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, range(0xA0, 0xA1), 0xA7, 0xA9, 0xAB, 0xBB, 0xBF, 0xC1, 0xC9, 0xCD, 0xD1, 0xD3, 0xDA, 0xDC, 0xE1, 0xE9, 0xED, 0xF1, 0xF3, 0xFA, 0xFC, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
deu: [range(0x20, 0x5F), range(0x61, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0xC4, 0xD6, 0xDC, 0xDF, 0xE4, 0xF6, 0xFC, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
fra: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xAB, range(0xB2, 0xB3), 0xBB, 0xC0, 0xC2, range(0xC6, 0xCB), range(0xCE, 0xCF), 0xD4, 0xD9, range(0xDB, 0xDC), 0xE0, 0xE2, range(0xE6, 0xEB), range(0xEE, 0xEF), 0xF4, 0xF9, range(0xFB, 0xFC), 0xFF, range(0x152, 0x153), 0x178, 0x2B3, 0x2E2, range(0x1D48, 0x1D49), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2019, range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, range(0x202F, 0x2030), 0x20AC, 0x2212],
ita: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x5F), range(0x61, 0x7D), 0xA0, 0xA9, 0xAB, 0xBB, 0xC0, range(0xC8, 0xC9), 0xCC, range(0xD2, 0xD3), 0xD9, 0xE0, range(0xE8, 0xE9), 0xEC, range(0xF2, 0xF3), 0xF9, 0x2011, 0x2014, 0x2019, range(0x201C, 0x201D), 0x2026, 0x2030, 0x20AC],
por: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0xC0, 0xC3), 0xC7, range(0xC9, 0xCA), 0xCD, range(0xD2, 0xD5), 0xDA, range(0xE0, 0xE3), 0xE7, range(0xE9, 0xEA), 0xED, range(0xF2, 0xF5), 0xFA, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
rus: [range(0x20, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0x401, range(0x410, 0x44F), 0x451, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
fin: [range(0x20, 0x21), range(0x23, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xBB, range(0xC4, 0xC5), 0xD6, range(0xE4, 0xE5), 0xF6, range(0x160, 0x161), range(0x17D, 0x17E), range(0x2010, 0x2011), 0x2013, 0x2019, 0x201D, 0x2026, range(0x202F, 0x2030), 0x20AC, 0x2212],
nld: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC1, 0xC4, 0xC9, 0xCB, 0xCD, 0xCF, 0xD3, 0xD6, 0xDA, 0xDC, 0xE1, 0xE4, 0xE9, 0xEB, 0xED, 0xEF, 0xF3, 0xF6, 0xFA, 0xFC, 0x133, 0x301, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
zho: [range(0x20, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xB7, range(0x2010, 0x2011), range(0x2013, 0x2016), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2025, 0x2026), 0x2030, range(0x2032, 0x2033), 0x2035, 0x203B, 0x20AC, range(0x3001, 0x3003), range(0x3007, 0x3011), range(0x3014, 0x3017), range(0x301D, 0x301E), range(0x4E00, 0x4E01), 0x4E03, range(0x4E07, 0x4E0E), 0x4E11, range(0x4E13, 0x4E14), 0x4E16, range(0x4E18, 0x4E1A), range(0x4E1C, 0x4E1D), 0x4E22, range(0x4E24, 0x4E25), 0x4E27, 0x4E2A, 0x4E2D, 0x4E30, 0x4E32, 0x4E34, range(0x4E38, 0x4E3B), range(0x4E3D, 0x4E3E), 0x4E43, 0x4E45, range(0x4E48, 0x4E49), range(0x4E4B, 0x4E50), 0x4E54, 0x4E56, range(0x4E58, 0x4E59), 0x4E5D, range(0x4E5F, 0x4E61), 0x4E66, range(0x4E70, 0x4E71), 0x4E7E, 0x4E86, range(0x4E88, 0x4E89), range(0x4E8B, 0x4E8C), range(0x4E8E, 0x4E8F), range(0x4E91, 0x4E92), range(0x4E94, 0x4E95), range(0x4E9A, 0x4E9B), 0x4EA1, range(0x4EA4, 0x4EA8), range(0x4EAB, 0x4EAC), 0x4EAE, 0x4EB2, 0x4EBA, range(0x4EBF, 0x4EC1), 0x4EC5, 0x4EC7, range(0x4ECA, 0x4ECB), range(0x4ECD, 0x4ECE), 0x4ED4, 0x4ED6, range(0x4ED8, 0x4ED9), range(0x4EE3, 0x4EE5), 0x4EEA, 0x4EEC, 0x4EF0, 0x4EF2, range(0x4EF6, 0x4EF7), 0x4EFB, 0x4EFD, 0x4EFF, 0x4F01, 0x4F0A, 0x4F0D, range(0x4F0F, 0x4F11), range(0x4F17, 0x4F1A), range(0x4F1F, 0x4F20), 0x4F24, 0x4F26, range(0x4F2F, 0x4F30), 0x4F34, 0x4F38, range(0x4F3C, 0x4F3D), 0x4F46, range(0x4F4D, 0x4F51), 0x4F53, 0x4F55, 0x4F59, range(0x4F5B, 0x4F5C), 0x4F60, 0x4F64, 0x4F69, 0x4F73, 0x4F7F, 0x4F8B, 0x4F9B, 0x4F9D, 0x4FA0, range(0x4FA6, 0x4FA8), 0x4FAC, 0x4FAF, 0x4FB5, 0x4FBF, range(0x4FC3, 0x4FC4), 0x4FCA, 0x4FD7, 0x4FDD, 0x4FE1, 0x4FE9, 0x4FEE, 0x4FF1, 0x4FFE, 0x500D, 0x5012, range(0x5019, 0x501A), 0x501F, 0x5026, 0x503C, 0x503E, 0x5047, 0x504C, 0x504F, 0x505A, 0x505C, 0x5065, range(0x5076, 0x5077), 0x50A8, 0x50AC, 0x50B2, 0x50BB, 0x50CF, 0x50E7, 0x5112, 0x513F, 0x5141, range(0x5143, 0x5146), range(0x5148, 0x5149), 0x514B, 0x514D, 0x5151, 0x5154, 0x515A, 0x5165, 0x5168, range(0x516B, 0x516E), range(0x5170, 0x5171), range(0x5173, 0x5179), range(0x517B, 0x517D), 0x5185, 0x5188, range(0x518C, 0x518D), 0x5192, 0x5199, range(0x519B, 0x519C), 0x51A0, 0x51AC, 0x51B0, range(0x51B2, 0x51B3), 0x51B5, 0x51B7, 0x51C6, 0x51CC, 0x51CF, 0x51DD, range(0x51E0, 0x51E1), 0x51E4, 0x51ED, range(0x51EF, 0x51F0), range(0x51FA, 0x51FB), 0x51FD, 0x5200, range(0x5206, 0x5207), 0x520A, range(0x5211, 0x5212), range(0x5217, 0x521B), 0x521D, 0x5224, 0x5229, 0x522B, 0x5230, range(0x5236, 0x5238), range(0x523A, 0x523B), 0x5242, 0x524D, 0x5251, 0x5267, range(0x5269, 0x526A), 0x526F, 0x5272, 0x529B, range(0x529D, 0x52A1), 0x52A3, range(0x52A8, 0x52AB), range(0x52B1, 0x52B3), 0x52BF, 0x52C7, 0x52C9, 0x52CB, 0x52D2, 0x52E4, range(0x52FE, 0x52FF), range(0x5305, 0x5306), 0x5308, range(0x5316, 0x5317), 0x5319, range(0x5339, 0x533B), 0x5341, 0x5343, range(0x5347, 0x5348), 0x534A, range(0x534E, 0x534F), range(0x5352, 0x5353), range(0x5355, 0x5357), 0x535A, range(0x5360, 0x5362), 0x536B, range(0x536F, 0x5371), range(0x5373, 0x5374), 0x5377, 0x5382, range(0x5384, 0x5386), 0x5389, range(0x538B, 0x538D), 0x539A, 0x539F, 0x53BB, 0x53BF, 0x53C2, range(0x53C8, 0x53CD), 0x53D1, 0x53D4, range(0x53D6, 0x53D9), range(0x53E3, 0x53E6), range(0x53EA, 0x53ED), range(0x53EF, 0x53F0), range(0x53F2, 0x53F3), range(0x53F6, 0x53F9), range(0x5403, 0x5404), range(0x5408, 0x540A), range(0x540C, 0x540E), range(0x5410, 0x5411), 0x5413, 0x5417, 0x541B, 0x541D, 0x541F, range(0x5426, 0x5427), range(0x542B, 0x542C), 0x542F, 0x5435, range(0x5438, 0x5439), 0x543B, 0x543E, 0x5440, 0x5446, 0x5448, 0x544A, 0x5450, 0x5458, 0x545C, 0x5462, 0x5466, 0x5468, 0x5473, 0x5475, range(0x547C, 0x547D), 0x548C, 0x5496, range(0x54A6, 0x54A8), 0x54AA, 0x54AC, 0x54AF, 0x54B1, range(0x54C0, 0x54C1), range(0x54C7, 0x54C9), range(0x54CD, 0x54CE), 0x54DF, range(0x54E5, 0x54E6), range(0x54E9, 0x54EA), 0x54ED, 0x54F2, 0x5509, 0x5510, 0x5524, 0x552C, range(0x552E, 0x552F), 0x5531, 0x5537, 0x5546, 0x554A, 0x5561, range(0x5565, 0x5566), 0x556A, 0x5580, 0x5582, 0x5584, 0x5587, 0x558A, 0x558F, 0x5594, range(0x559C, 0x559D), 0x55B5, 0x55B7, 0x55BB, 0x55D2, 0x55E8, 0x55EF, 0x5609, 0x561B, 0x5634, 0x563B, 0x563F, 0x5668, 0x56DB, 0x56DE, 0x56E0, 0x56E2, 0x56ED, 0x56F0, 0x56F4, 0x56FA, range(0x56FD, 0x56FE), 0x5706, 0x5708, 0x571F, 0x5723, 0x5728, 0x572D, 0x5730, 0x5733, 0x573A, 0x573E, 0x5740, 0x5747, 0x574E, range(0x5750, 0x5751), 0x5757, range(0x575A, 0x575C), 0x5761, 0x5764, 0x5766, 0x576A, range(0x5782, 0x5783), 0x578B, 0x5792, 0x57C3, 0x57CB, 0x57CE, 0x57D4, 0x57DF, range(0x57F9, 0x57FA), 0x5802, 0x5806, 0x5815, 0x5821, 0x582A, 0x5851, 0x5854, 0x585E, 0x586B, 0x5883, 0x589E, 0x58A8, 0x58C1, 0x58E4, range(0x58EB, 0x58EC), 0x58EE, 0x58F0, 0x5904, 0x5907, 0x590D, 0x590F, range(0x5915, 0x5916), 0x591A, 0x591C, 0x591F, 0x5925, 0x5927, range(0x5929, 0x592B), 0x592E, 0x5931, 0x5934, range(0x5937, 0x593A), range(0x5947, 0x5949), 0x594B, 0x594F, 0x5951, 0x5954, range(0x5956, 0x5957), 0x5965, range(0x5973, 0x5974), 0x5976, 0x5979, 0x597D, 0x5982, range(0x5987, 0x5988), 0x5996, 0x5999, 0x59A5, 0x59A8, 0x59AE, 0x59B9, 0x59BB, 0x59C6, range(0x59CA, 0x59CB), range(0x59D0, 0x59D1), range(0x59D3, 0x59D4), 0x59FF, 0x5A01, range(0x5A03, 0x5A04), 0x5A18, 0x5A1C, 0x5A1F, 0x5A31, 0x5A46, 0x5A5A, 0x5A92, 0x5AC1, 0x5ACC, 0x5AE9, 0x5B50, range(0x5B54, 0x5B55), range(0x5B57, 0x5B59), range(0x5B5C, 0x5B5D), 0x5B5F, range(0x5B63, 0x5B64), 0x5B66, 0x5B69, 0x5B81, 0x5B83, range(0x5B87, 0x5B89), range(0x5B8B, 0x5B8C), 0x5B8F, range(0x5B97, 0x5B9E), range(0x5BA1, 0x5BA4), 0x5BAA, range(0x5BB3, 0x5BB4), 0x5BB6, 0x5BB9, range(0x5BBD, 0x5BBF), 0x5BC2, range(0x5BC4, 0x5BC7), 0x5BCC, 0x5BD2, range(0x5BDD, 0x5BDF), 0x5BE1, 0x5BE8, range(0x5BF8, 0x5BF9), range(0x5BFB, 0x5BFC), 0x5BFF, 0x5C01, 0x5C04, 0x5C06, 0x5C0A, 0x5C0F, 0x5C11, 0x5C14, 0x5C16, 0x5C18, 0x5C1A, 0x5C1D, 0x5C24, 0x5C31, 0x5C3A, range(0x5C3C, 0x5C3E), range(0x5C40, 0x5C42), 0x5C45, 0x5C4B, 0x5C4F, 0x5C55, 0x5C5E, 0x5C60, 0x5C71, range(0x5C81, 0x5C82), range(0x5C97, 0x5C98), range(0x5C9A, 0x5C9B), 0x5CB3, 0x5CB8, 0x5CE1, 0x5CF0, 0x5D07, 0x5D29, 0x5D34, range(0x5DDD, 0x5DDE), 0x5DE1, range(0x5DE5, 0x5DE8), 0x5DEB, 0x5DEE, range(0x5DF1, 0x5DF4), 0x5DF7, range(0x5E01, 0x5E03), 0x5E05, 0x5E08, 0x5E0C, 0x5E10, range(0x5E15, 0x5E16), 0x5E1D, 0x5E26, range(0x5E2D, 0x5E2E), 0x5E38, 0x5E3D, 0x5E45, 0x5E55, range(0x5E72, 0x5E74), 0x5E76, 0x5E78, range(0x5E7B, 0x5E7D), 0x5E7F, 0x5E86, 0x5E8A, 0x5E8F, range(0x5E93, 0x5E95), 0x5E97, range(0x5E99, 0x5E9A), 0x5E9C, range(0x5E9E, 0x5E9F), range(0x5EA6, 0x5EA7), 0x5EAD, range(0x5EB7, 0x5EB8), 0x5EC9, 0x5ED6, range(0x5EF6, 0x5EF7), 0x5EFA, 0x5F00, range(0x5F02, 0x5F04), 0x5F0A, 0x5F0F, 0x5F15, range(0x5F17, 0x5F18), range(0x5F1F, 0x5F20), range(0x5F25, 0x5F26), 0x5F2F, 0x5F31, range(0x5F39, 0x5F3A), range(0x5F52, 0x5F53), 0x5F55, 0x5F5D, 0x5F62, 0x5F69, range(0x5F6C, 0x5F6D), range(0x5F70, 0x5F71), 0x5F77, 0x5F79, range(0x5F7B, 0x5F7C), range(0x5F80, 0x5F81), range(0x5F84, 0x5F85), 0x5F88, range(0x5F8B, 0x5F8C), 0x5F90, 0x5F92, 0x5F97, 0x5FAA, 0x5FAE, 0x5FB5, 0x5FB7, 0x5FC3, range(0x5FC5, 0x5FC6), range(0x5FCC, 0x5FCD), range(0x5FD7, 0x5FD9), 0x5FE0, 0x5FE7, 0x5FEB, 0x5FF5, 0x5FFD, range(0x6000, 0x6001), 0x600E, 0x6012, range(0x6015, 0x6016), 0x601D, 0x6021, 0x6025, range(0x6027, 0x6028), 0x602A, 0x603B, 0x604B, 0x6050, 0x6062, range(0x6068, 0x6069), 0x606D, range(0x606F, 0x6070), 0x6076, 0x607C, 0x6084, 0x6089, 0x6094, range(0x609F, 0x60A0), 0x60A3, 0x60A8, 0x60B2, 0x60C5, 0x60D1, 0x60DC, 0x60E0, range(0x60E7, 0x60E8), 0x60EF, 0x60F3, 0x60F9, 0x6101, range(0x6108, 0x6109), 0x610F, 0x611A, 0x611F, 0x6127, 0x6148, 0x614E, 0x6155, 0x6162, 0x6167, 0x6170, 0x61BE, 0x61C2, 0x61D2, 0x6208, 0x620A, 0x620C, range(0x620F, 0x6212), 0x6216, 0x6218, 0x622A, 0x6234, 0x6237, range(0x623F, 0x6241), 0x6247, 0x624B, range(0x624D, 0x624E), 0x6251, 0x6253, 0x6258, 0x6263, 0x6267, 0x6269, range(0x626B, 0x626F), 0x6279, range(0x627E, 0x6280), 0x6284, 0x628A, 0x6291, 0x6293, 0x6295, range(0x6297, 0x6298), 0x62A2, range(0x62A4, 0x62A5), range(0x62AB, 0x62AC), 0x62B1, 0x62B5, 0x62B9, 0x62BD, range(0x62C5, 0x62C6), 0x62C9, 0x62CD, 0x62D2, 0x62D4, 0x62D6, 0x62D8, range(0x62DB, 0x62DC), 0x62DF, range(0x62E5, 0x62E6), range(0x62E8, 0x62E9), 0x62EC, 0x62F3, 0x62F7, 0x62FC, range(0x62FE, 0x62FF), 0x6301, 0x6307, 0x6309, 0x6311, 0x6316, 0x631D, 0x6321, range(0x6324, 0x6325), 0x632A, 0x632F, 0x633A, 0x6349, 0x6350, 0x6355, 0x635F, range(0x6361, 0x6362), 0x636E, 0x6377, range(0x6388, 0x6389), 0x638C, 0x6392, 0x63A2, 0x63A5, range(0x63A7, 0x63AA), 0x63B8, range(0x63CF, 0x63D0), 0x63D2, 0x63E1, 0x63F4, 0x641C, 0x641E, range(0x642C, 0x642D), 0x6444, 0x6446, 0x644A, 0x6454, 0x6458, 0x6469, 0x6478, 0x6492, 0x649E, 0x64AD, range(0x64CD, 0x64CE), 0x64E6, 0x652F, 0x6536, 0x6539, 0x653B, range(0x653E, 0x653F), 0x6545, 0x6548, 0x654C, 0x654F, 0x6551, 0x6559, 0x655D, range(0x6562, 0x6563), 0x6566, 0x656C, 0x6570, 0x6572, 0x6574, 0x6587, 0x658B, 0x6590, 0x6597, 0x6599, 0x659C, 0x65A5, 0x65AD, range(0x65AF, 0x65B0), 0x65B9, range(0x65BC, 0x65BD), 0x65C1, 0x65C5, 0x65CB, 0x65CF, 0x65D7, 0x65E0, 0x65E2, range(0x65E5, 0x65E9), 0x65ED, 0x65F6, 0x65FA, 0x6602, 0x6606, 0x660C, range(0x660E, 0x660F), 0x6613, range(0x661F, 0x6620), 0x6625, 0x6628, 0x662D, 0x662F, 0x663E, 0x6643, 0x664B, range(0x6652, 0x6653), 0x665A, 0x6668, range(0x666E, 0x666F), 0x6674, 0x6676, 0x667A, 0x6682, 0x6691, range(0x6696, 0x6697), 0x66AE, 0x66B4, 0x66F0, 0x66F2, 0x66F4, 0x66F9, 0x66FC, range(0x66FE, 0x6700), range(0x6708, 0x6709), 0x670B, 0x670D, 0x6717, 0x671B, 0x671D, 0x671F, 0x6728, range(0x672A, 0x672D), 0x672F, 0x6731, 0x6735, 0x673A, 0x6740, range(0x6742, 0x6743), 0x6749, 0x674E, range(0x6750, 0x6751), 0x675C, 0x675F, 0x6761, 0x6765, 0x6768, range(0x676F, 0x6770), range(0x677E, 0x677F), 0x6781, 0x6784, 0x6790, 0x6797, range(0x679C, 0x679D), 0x67A2, range(0x67AA, 0x67AB), 0x67B6, range(0x67CF, 0x67D0), range(0x67D3, 0x67D4), 0x67E5, 0x67EC, 0x67EF, range(0x67F3, 0x67F4), 0x6807, 0x680B, 0x680F, 0x6811, 0x6821, range(0x6837, 0x6839), 0x683C, 0x6843, 0x6846, 0x6848, 0x684C, 0x6851, 0x6863, 0x6865, 0x6881, 0x6885, 0x68A6, range(0x68AF, 0x68B0), 0x68B5, 0x68C0, 0x68C9, 0x68CB, 0x68D2, 0x68DA, 0x68EE, 0x6905, 0x690D, 0x6930, 0x695A, 0x697C, 0x6982, 0x699C, 0x6A21, 0x6A31, 0x6A80, range(0x6B20, 0x6B23), 0x6B27, 0x6B32, 0x6B3A, 0x6B3E, 0x6B49, 0x6B4C, range(0x6B62, 0x6B66), 0x6B6A, 0x6B7B, range(0x6B8A, 0x6B8B), 0x6BB5, 0x6BC5, 0x6BCD, 0x6BCF, 0x6BD2, range(0x6BD4, 0x6BD5), 0x6BDB, 0x6BEB, 0x6C0F, 0x6C11, 0x6C14, 0x6C1B, 0x6C34, 0x6C38, 0x6C42, 0x6C47, 0x6C49, 0x6C57, 0x6C5D, range(0x6C5F, 0x6C61), 0x6C64, 0x6C6A, 0x6C76, 0x6C7D, 0x6C83, range(0x6C88, 0x6C89), 0x6C99, 0x6C9F, 0x6CA1, 0x6CA7, 0x6CB3, 0x6CB9, 0x6CBB, 0x6CBF, range(0x6CC9, 0x6CCA), 0x6CD5, 0x6CDB, range(0x6CE1, 0x6CE3), 0x6CE5, 0x6CE8, 0x6CF0, 0x6CF3, 0x6CFD, 0x6D0B, 0x6D17, 0x6D1B, 0x6D1E, 0x6D25, 0x6D2A, 0x6D32, 0x6D3B, range(0x6D3D, 0x6D3E), 0x6D41, 0x6D45, 0x6D4B, range(0x6D4E, 0x6D4F), 0x6D51, 0x6D53, 0x6D59, 0x6D66, range(0x6D69, 0x6D6A), 0x6D6E, 0x6D74, 0x6D77, 0x6D85, range(0x6D88, 0x6D89), 0x6D9B, 0x6DA8, 0x6DAF, 0x6DB2, 0x6DB5, 0x6DCB, 0x6DD1, 0x6DD8, 0x6DE1, 0x6DF1, 0x6DF7, 0x6DFB, 0x6E05, 0x6E10, 0x6E21, 0x6E23, 0x6E29, 0x6E2F, 0x6E34, 0x6E38, 0x6E56, 0x6E7E, 0x6E90, 0x6E9C, 0x6EAA, 0x6ECB, 0x6ED1, 0x6EE1, 0x6EE5, 0x6EE8, 0x6EF4, 0x6F02, 0x6F0F, 0x6F14, 0x6F20, 0x6F2B, 0x6F58, 0x6F5C, 0x6F6E, 0x6F8E, 0x6FB3, 0x6FC0, 0x704C, 0x706B, 0x706D, range(0x706F, 0x7070), 0x7075, 0x707F, 0x7089, 0x708E, 0x70AE, range(0x70B8, 0x70B9), 0x70C2, 0x70C8, 0x70E4, range(0x70E6, 0x70E7), 0x70ED, 0x7126, 0x7136, 0x714C, 0x715E, 0x7167, 0x716E, 0x718A, 0x719F, 0x71C3, 0x71D5, 0x7206, 0x722A, 0x722C, 0x7231, range(0x7235, 0x7238), 0x723D, range(0x7247, 0x7248), 0x724C, 0x7259, 0x725B, range(0x7261, 0x7262), 0x7267, 0x7269, 0x7272, 0x7275, range(0x7279, 0x727A), 0x72AF, 0x72B6, 0x72B9, 0x72C2, 0x72D0, 0x72D7, 0x72E0, 0x72EC, 0x72EE, 0x72F1, 0x72FC, range(0x731B, 0x731C), 0x732A, 0x732E, 0x7334, 0x7384, 0x7387, 0x7389, 0x738B, 0x739B, 0x73A9, 0x73AB, range(0x73AF, 0x73B0), 0x73B2, 0x73BB, 0x73C0, 0x73CA, 0x73CD, 0x73E0, 0x73ED, 0x7403, 0x7406, 0x740A, 0x742A, range(0x7433, 0x7434), 0x743C, 0x7459, 0x745C, range(0x745E, 0x745F), 0x7470, 0x7476, 0x7483, 0x74DC, 0x74E6, 0x74F6, 0x7518, 0x751A, 0x751C, 0x751F, 0x7528, range(0x7530, 0x7533), 0x7535, range(0x7537, 0x7538), 0x753B, 0x7545, 0x754C, 0x7559, 0x7565, 0x756A, 0x7586, 0x758F, 0x7591, 0x7597, 0x75AF, 0x75B2, 0x75BC, 0x75BE, 0x75C5, 0x75D5, 0x75DB, 0x75F4, 0x7678, 0x767B, range(0x767D, 0x767E), 0x7684, range(0x7686, 0x7687), 0x76AE, 0x76C8, 0x76CA, range(0x76D1, 0x76D2), 0x76D6, 0x76D8, 0x76DB, 0x76DF, 0x76EE, 0x76F4, 0x76F8, 0x76FC, 0x76FE, 0x7701, 0x7709, 0x770B, range(0x771F, 0x7720), 0x773C, 0x7740, 0x775B, 0x7761, 0x7763, 0x77A7, 0x77DB, 0x77E3, 0x77E5, 0x77ED, 0x77F3, 0x77F6, range(0x7801, 0x7802), 0x780D, 0x7814, 0x7834, 0x7840, 0x7855, 0x786C, 0x786E, range(0x788D, 0x788E), 0x7897, 0x789F, 0x78A7, 0x78B0, 0x78C1, 0x78C5, 0x78E8, 0x793A, 0x793C, 0x793E, 0x7956, 0x795A, range(0x795D, 0x795E), 0x7965, 0x7968, 0x796F, 0x7978, 0x7981, 0x7985, 0x798F, 0x79BB, range(0x79C0, 0x79C1), 0x79CB, 0x79CD, range(0x79D1, 0x79D2), 0x79D8, 0x79DF, 0x79E4, 0x79E6, 0x79E9, range(0x79EF, 0x79F0), 0x79FB, 0x7A00, 0x7A0B, range(0x7A0D, 0x7A0E), 0x7A23, 0x7A33, 0x7A3F, 0x7A46, range(0x7A76, 0x7A77), range(0x7A79, 0x7A7A), 0x7A7F, 0x7A81, 0x7A97, 0x7A9D, 0x7ACB, 0x7AD9, range(0x7ADE, 0x7AE0), 0x7AE5, 0x7AEF, 0x7AF9, 0x7B11, 0x7B14, 0x7B1B, 0x7B26, 0x7B28, 0x7B2C, 0x7B49, 0x7B4B, 0x7B51, 0x7B54, 0x7B56, 0x7B79, 0x7B7E, 0x7B80, 0x7B97, 0x7BA1, 0x7BAD, 0x7BB1, 0x7BC7, 0x7BEE, 0x7C3F, 0x7C4D, 0x7C73, 0x7C7B, 0x7C89, 0x7C92, 0x7C97, 0x7CA4, 0x7CB9, 0x7CBE, 0x7CCA, range(0x7CD5, 0x7CD6), 0x7CDF, 0x7CFB, 0x7D20, 0x7D22, 0x7D27, 0x7D2B, 0x7D2F, 0x7E41, 0x7EA2, range(0x7EA6, 0x7EA7), 0x7EAA, 0x7EAF, range(0x7EB2, 0x7EB3), 0x7EB5, range(0x7EB7, 0x7EB8), 0x7EBD, 0x7EBF, range(0x7EC3, 0x7EC4), range(0x7EC6, 0x7EC8), 0x7ECD, 0x7ECF, 0x7ED3, 0x7ED5, range(0x7ED8, 0x7ED9), range(0x7EDC, 0x7EDD), 0x7EDF, 0x7EE7, range(0x7EE9, 0x7EEA), 0x7EED, range(0x7EF4, 0x7EF5), 0x7EFC, 0x7EFF, 0x7F05, 0x7F13, 0x7F16, 0x7F18, 0x7F20, 0x7F29, 0x7F34, 0x7F36, 0x7F38, 0x7F3A, range(0x7F50, 0x7F51), 0x7F55, 0x7F57, 0x7F5A, 0x7F62, 0x7F6A, 0x7F6E, 0x7F72, 0x7F8A, 0x7F8E, 0x7F9E, 0x7FA4, 0x7FAF, 0x7FBD, 0x7FC1, 0x7FC5, 0x7FD4, 0x7FD8, 0x7FE0, 0x7FF0, range(0x7FFB, 0x7FFC), range(0x8000, 0x8001), 0x8003, 0x8005, range(0x800C, 0x800D), 0x8010, 0x8017, 0x8033, 0x8036, 0x804A, 0x804C, 0x8054, 0x8058, 0x805A, 0x806A, 0x8089, 0x8096, 0x809A, 0x80A1, range(0x80A4, 0x80A5), 0x80A9, 0x80AF, 0x80B2, 0x80C1, 0x80C6, 0x80CC, 0x80CE, 0x80D6, 0x80DC, 0x80DE, 0x80E1, 0x80F6, 0x80F8, 0x80FD, 0x8106, 0x8111, 0x8131, 0x8138, 0x814A, 0x8150, 0x8153, 0x8170, 0x8179, range(0x817E, 0x817F), 0x81C2, 0x81E3, 0x81EA, 0x81ED, range(0x81F3, 0x81F4), range(0x820C, 0x820D), 0x8212, range(0x821E, 0x821F), 0x822A, 0x822C, 0x8230, 0x8239, 0x826F, 0x8272, 0x827A, 0x827E, 0x8282, 0x8292, 0x829D, 0x82A6, range(0x82AC, 0x82AD), 0x82B1, 0x82B3, 0x82CD, 0x82CF, 0x82D7, range(0x82E5, 0x82E6), 0x82F1, range(0x8302, 0x8303), 0x8328, 0x832B, 0x8336, 0x8349, 0x8350, 0x8352, 0x8363, 0x836F, 0x8377, 0x8389, 0x838E, range(0x83AA, 0x83AB), range(0x83B1, 0x83B2), 0x83B7, 0x83DC, 0x83E9, 0x83F2, 0x8404, 0x840D, range(0x8424, 0x8425), range(0x8427, 0x8428), 0x843D, 0x8457, 0x845B, 0x8461, 0x8482, 0x848B, 0x8499, 0x84C9, 0x84DD, 0x84EC, 0x8511, 0x8521, 0x8584, 0x85AA, 0x85C9, 0x85CF, 0x85E4, 0x864E, 0x8651, 0x866B, 0x8679, range(0x867D, 0x867E), 0x8681, 0x86C7, 0x86CB, 0x86D9, 0x86EE, 0x8702, 0x871C, 0x8776, 0x878D, 0x87F9, 0x8822, 0x8840, 0x884C, 0x8857, 0x8861, 0x8863, 0x8865, 0x8868, 0x888B, 0x88AB, 0x88AD, range(0x88C1, 0x88C2), 0x88C5, 0x88D5, 0x88E4, 0x897F, 0x8981, 0x8986, range(0x89C1, 0x89C2), 0x89C4, 0x89C6, range(0x89C8, 0x89C9), 0x89D2, 0x89E3, 0x8A00, 0x8A89, 0x8A93, 0x8B66, range(0x8BA1, 0x8BA2), 0x8BA4, range(0x8BA8, 0x8BA9), range(0x8BAD, 0x8BB0), 0x8BB2, range(0x8BB7, 0x8BB8), 0x8BBA, range(0x8BBE, 0x8BBF), 0x8BC1, 0x8BC4, 0x8BC6, 0x8BC9, 0x8BCD, 0x8BD1, 0x8BD5, 0x8BD7, 0x8BDA, range(0x8BDD, 0x8BDE), 0x8BE2, range(0x8BE5, 0x8BE6), 0x8BED, 0x8BEF, 0x8BF4, range(0x8BF7, 0x8BF8), range(0x8BFA, 0x8BFB), 0x8BFE, 0x8C01, 0x8C03, 0x8C05, 0x8C08, range(0x8C0A, 0x8C0B), 0x8C13, 0x8C1C, 0x8C22, 0x8C28, 0x8C31, 0x8C37, 0x8C46, 0x8C61, 0x8C6A, 0x8C8C, range(0x8D1D, 0x8D1F), range(0x8D21, 0x8D25), range(0x8D27, 0x8D2A), 0x8D2D, 0x8D2F, 0x8D31, range(0x8D34, 0x8D35), range(0x8D38, 0x8D3A), 0x8D3C, 0x8D3E, 0x8D44, range(0x8D4B, 0x8D4C), range(0x8D4F, 0x8D50), 0x8D54, 0x8D56, range(0x8D5A, 0x8D5B), 0x8D5E, 0x8D60, 0x8D62, 0x8D64, 0x8D6B, 0x8D70, 0x8D75, 0x8D77, 0x8D81, 0x8D85, range(0x8D8A, 0x8D8B), 0x8DA3, 0x8DB3, 0x8DC3, 0x8DCC, 0x8DD1, 0x8DDD, 0x8DDF, 0x8DEF, 0x8DF3, 0x8E0F, 0x8E22, 0x8E29, 0x8EAB, 0x8EB2, 0x8F66, range(0x8F68, 0x8F69), 0x8F6C, range(0x8F6E, 0x8F70), 0x8F7B, 0x8F7D, 0x8F83, range(0x8F85, 0x8F86), range(0x8F88, 0x8F89), 0x8F91, 0x8F93, 0x8F9B, 0x8F9E, range(0x8FA8, 0x8FA9), range(0x8FB0, 0x8FB1), 0x8FB9, 0x8FBE, 0x8FC1, 0x8FC5, range(0x8FC7, 0x8FC8), 0x8FCE, range(0x8FD0, 0x8FD1), 0x8FD4, range(0x8FD8, 0x8FD9), range(0x8FDB, 0x8FDF), 0x8FE6, range(0x8FEA, 0x8FEB), 0x8FF0, 0x8FF7, 0x8FFD, range(0x9000, 0x9003), 0x9006, range(0x9009, 0x900A), range(0x900F, 0x9010), 0x9012, 0x9014, range(0x901A, 0x901B), 0x901D, range(0x901F, 0x9020), 0x9022, 0x9038, range(0x903B, 0x903C), 0x9047, 0x904D, 0x9053, 0x9057, range(0x906D, 0x906E), 0x9075, range(0x907F, 0x9080), 0x9093, 0x90A3, 0x90A6, 0x90AA, 0x90AE, 0x90B1, 0x90BB, 0x90CE, 0x90D1, 0x90E8, 0x90ED, 0x90FD, 0x9102, 0x9149, 0x914B, 0x914D, 0x9152, range(0x9177, 0x9178), 0x9189, 0x9192, 0x91C7, 0x91CA, range(0x91CC, 0x91CF), 0x91D1, 0x9488, 0x9493, 0x949F, 0x94A2, 0x94A6, 0x94B1, 0x94BB, 0x94C1, 0x94C3, 0x94DC, 0x94E2, 0x94ED, 0x94F6, 0x94FA, 0x94FE, range(0x9500, 0x9501), 0x9505, 0x950B, 0x9519, 0x9521, 0x9526, 0x952E, 0x953A, 0x9547, 0x955C, 0x956D, 0x957F, 0x95E8, 0x95EA, range(0x95ED, 0x95EE), 0x95F0, 0x95F2, 0x95F4, 0x95F7, 0x95F9, 0x95FB, 0x9601, 0x9605, 0x9610, 0x9614, 0x961F, 0x962E, range(0x9632, 0x9636), 0x963B, range(0x963F, 0x9640), range(0x9644, 0x9646), 0x9648, 0x964D, 0x9650, 0x9662, 0x9664, range(0x9669, 0x966A), range(0x9675, 0x9677), 0x9686, range(0x968F, 0x9690), 0x9694, 0x969C, 0x96BE, range(0x96C4, 0x96C6), 0x96C9, 0x96E8, 0x96EA, 0x96EF, 0x96F3, range(0x96F6, 0x96F7), 0x96FE, 0x9700, 0x9707, 0x970D, 0x9716, 0x9732, range(0x9738, 0x9739), 0x9752, 0x9756, 0x9759, 0x975E, 0x9760, 0x9762, 0x9769, 0x977C, 0x978B, 0x9791, 0x97E6, 0x97E9, 0x97F3, range(0x9875, 0x9876), range(0x9879, 0x987B), range(0x987D, 0x987F), 0x9884, range(0x9886, 0x9887), 0x9891, range(0x9897, 0x9898), 0x989D, 0x98CE, range(0x98D8, 0x98D9), range(0x98DE, 0x98DF), 0x9910, range(0x996D, 0x996E), range(0x9970, 0x9971), 0x997C, 0x9986, 0x9996, 0x9999, 0x99A8, 0x9A6C, 0x9A71, 0x9A76, 0x9A7B, 0x9A7E, 0x9A8C, 0x9A91, 0x9A97, 0x9A9A, 0x9AA4, 0x9AA8, 0x9AD8, 0x9B3C, 0x9B42, 0x9B45, 0x9B54, 0x9C7C, 0x9C81, 0x9C9C, 0x9E1F, 0x9E21, 0x9E23, 0x9E2D, 0x9E3F, 0x9E45, 0x9E64, 0x9E70, 0x9E7F, 0x9EA6, 0x9EBB, 0x9EC4, 0x9ECE, 0x9ED1, 0x9ED8, 0x9F13, 0x9F20, 0x9F3B, 0x9F50, 0x9F7F, 0x9F84, 0x9F99, 0x9F9F, range(0xFE30, 0xFE31), range(0xFE33, 0xFE44), range(0xFE49, 0xFE52), range(0xFE54, 0xFE57), range(0xFE59, 0xFE61), 0xFE63, 0xFE68, range(0xFE6A, 0xFE6B), range(0xFF01, 0xFF03), range(0xFF05, 0xFF0A), range(0xFF0C, 0xFF0F), range(0xFF1A, 0xFF1B), range(0xFF1F, 0xFF20), range(0xFF3B, 0xFF3D), 0xFF3F, 0xFF5B, 0xFF5D],
swe: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC0, range(0xC4, 0xC5), 0xC9, 0xD6, 0xE0, range(0xE4, 0xE5), 0xE9, 0xF6, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC, 0x2212],
pol: [range(0x20, 0x5F), range(0x61, 0x70), range(0x72, 0x75), 0x77, range(0x79, 0x7E), 0xA0, 0xA7, 0xA9, 0xAB, 0xB0, 0xBB, 0xD3, 0xF3, range(0x104, 0x107), range(0x118, 0x119), range(0x141, 0x144), range(0x15A, 0x15B), range(0x179, 0x17C), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x201D, 0x201E), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
kor: [range(0x20, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), range(0xA0, 0xA1), 0xA7, 0xA9, range(0xB6, 0xB7), 0xBF, range(0x2010, 0x2011), range(0x2014, 0x2015), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), range(0x2025, 0x2026), 0x2030, range(0x2032, 0x2033), 0x203B, 0x203E, 0x20AC, range(0x3001, 0x3003), range(0x3008, 0x3011), range(0x3014, 0x3015), 0x301C, 0x30FB, 0x3131, 0x3134, 0x3137, 0x3139, range(0x3141, 0x3142), 0x3145, range(0x3147, 0x3148), range(0x314A, 0x314E), range(0xAC00, 0xD7A3), range(0xFF01, 0xFF03), range(0xFF05, 0xFF0A), range(0xFF0C, 0xFF0F), range(0xFF1A, 0xFF1B), range(0xFF1F, 0xFF20), range(0xFF3B, 0xFF3D), 0xFF3F, 0xFF5B, 0xFF5D],
tur: [range(0x20, 0x5F), range(0x61, 0x70), range(0x72, 0x76), range(0x79, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC7, 0xD6, 0xDC, 0xE7, 0xF6, 0xFC, range(0x11E, 0x11F), range(0x130, 0x131), range(0x15E, 0x15F), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
hin: [range(0x20, 0x25), range(0x27, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, range(0x901, 0x903), range(0x905, 0x90D), range(0x90F, 0x911), range(0x913, 0x928), range(0x92A, 0x930), range(0x932, 0x933), range(0x935, 0x939), range(0x93C, 0x943), 0x945, range(0x947, 0x949), range(0x94B, 0x94D), 0x950, range(0x964, 0x970), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
ell: [range(0x20, 0x22), range(0x24, 0x3E), 0x40, range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0x301, 0x308, 0x386, range(0x388, 0x38A), 0x38C, range(0x38E, 0x3A1), range(0x3A3, 0x3CE), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2026, 0x2030, 0x20AC],
nor: [range(0x20, 0x21), range(0x23, 0x25), range(0x27, 0x5F), range(0x61, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, range(0xBF, 0xC0), range(0xC5, 0xC6), 0xC9, range(0xD2, 0xD4), 0xD8, 0xE0, range(0xE5, 0xE6), 0xE9, range(0xF2, 0xF4), 0xF8, 0x2011, 0x2013, range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC, 0x2212],
hun: [range(0x20, 0x5F), range(0x61, 0x70), range(0x72, 0x76), range(0x79, 0x7E), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0xC1, 0xC9, 0xCD, 0xD3, 0xD6, 0xDA, 0xDC, 0xE1, 0xE9, 0xED, 0xF3, 0xF6, 0xFA, 0xFC, range(0x150, 0x151), range(0x170, 0x171), 0x1F1, 0x1F3, 0x2011, 0x2013, 0x2019, range(0x201D, 0x201E), 0x2026, 0x2030, 0x2052, 0x20AC, range(0x27E8, 0x27E9)],
ces: [range(0x20, 0x21), range(0x24, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC1, 0xC9, 0xCD, 0xD3, 0xDA, 0xDD, 0xE1, 0xE9, 0xED, 0xF3, 0xFA, 0xFD, range(0x10C, 0x10F), range(0x11A, 0x11B), range(0x147, 0x148), range(0x158, 0x159), range(0x160, 0x161), range(0x164, 0x165), range(0x16E, 0x16F), range(0x17D, 0x17E), range(0x2010, 0x2011), 0x2013, 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
dan: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0xC5, 0xC6), 0xD8, range(0xE5, 0xE6), 0xF8, range(0x2010, 0x2011), 0x2013, range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2020, 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
ind: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
est: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x5F), range(0x61, 0x7D), 0xA0, 0xA9, 0xC4, range(0xD5, 0xD6), 0xDC, 0xE4, range(0xF5, 0xF6), 0xFC, range(0x160, 0x161), range(0x17D, 0x17E), 0x2011, 0x2013, 0x201C, 0x201E, 0x2030, 0x20AC, 0x2212],
ara: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3A), range(0x3C, 0x3E), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, 0xAB, 0xBB, 0x609, 0x60C, range(0x61B, 0x61C), 0x61F, range(0x621, 0x63A), range(0x641, 0x652), range(0x660, 0x66C), 0x670, 0x200E, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2026, 0x2030, 0x20AC],
ron: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA9, 0xAB, 0xBB, 0xC2, 0xCE, 0xE2, 0xEE, range(0x102, 0x103), range(0x218, 0x21B), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, range(0x201C, 0x201E), 0x2026, 0x2030, 0x20AC],
lat: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
srp: [range(0x20, 0x21), range(0x23, 0x25), range(0x27, 0x3F), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA9, 0x402, range(0x408, 0x40B), range(0x40F, 0x418), range(0x41A, 0x428), range(0x430, 0x438), range(0x43A, 0x448), 0x452, range(0x458, 0x45B), 0x45F, range(0x2010, 0x2011), 0x2013, 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
pan: [range(0x20, 0x22), range(0x24, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, range(0xA05, 0xA0A), range(0xA0F, 0xA10), range(0xA13, 0xA28), range(0xA2A, 0xA30), 0xA32, range(0xA35, 0xA36), range(0xA38, 0xA39), 0xA3C, range(0xA3E, 0xA42), range(0xA47, 0xA48), range(0xA4B, 0xA4D), range(0xA59, 0xA5C), 0xA5E, range(0xA66, 0xA74), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2030, range(0x2032, 0x2033), 0x20AC],
heb: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, 0x5BE, range(0x5D0, 0x5EA), range(0x5F3, 0x5F4), 0x200E, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2030, 0x20AC],
slv: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x5F), range(0x61, 0x70), range(0x72, 0x76), range(0x7A, 0x7D), 0xA0, 0xA9, 0xAB, 0xBB, 0x106, range(0x10C, 0x10D), 0x110, range(0x160, 0x161), range(0x17D, 0x17E), 0x2011, 0x2013, range(0x201E, 0x201F), 0x2026, 0x2030, 0x20AC, 0x2212],
cat: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, range(0xA0, 0xA1), 0xA7, 0xA9, 0xAB, 0xB7, 0xBB, range(0xBF, 0xC0), range(0xC7, 0xC9), 0xCD, 0xCF, range(0xD2, 0xD3), 0xDA, 0xDC, 0xE0, range(0xE7, 0xE9), 0xED, 0xEF, range(0xF2, 0xF3), 0xFA, 0xFC, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
tam: [range(0x20, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, 0xB83, range(0xB85, 0xB8A), range(0xB8E, 0xB90), range(0xB92, 0xB95), range(0xB99, 0xB9A), 0xB9C, range(0xB9E, 0xB9F), range(0xBA3, 0xBA4), range(0xBA8, 0xBAA), range(0xBAE, 0xBB5), range(0xBB7, 0xBB9), range(0xBBE, 0xBC2), range(0xBC6, 0xBC8), range(0xBCA, 0xBCD), range(0xBE6, 0xBEF), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
lav: [range(0x20, 0x5F), range(0x61, 0x70), range(0x72, 0x76), 0x7A, 0x7C, 0xA0, 0xA7, 0xA9, range(0x100, 0x101), range(0x10C, 0x10D), range(0x112, 0x113), range(0x122, 0x123), range(0x12A, 0x12B), range(0x136, 0x137), range(0x13B, 0x13C), range(0x145, 0x146), range(0x160, 0x161), range(0x16A, 0x16B), range(0x17D, 0x17E), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x201A), range(0x201C, 0x201E), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
hrv: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x5F), range(0x61, 0x70), range(0x72, 0x76), 0x7A, 0x7C, 0xA0, 0xA9, range(0x106, 0x107), range(0x10C, 0x10D), range(0x110, 0x111), range(0x160, 0x161), range(0x17D, 0x17E), 0x1C4, range(0x1C6, 0x1C7), range(0x1C9, 0x1CA), 0x1CC, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x201A), range(0x201C, 0x201E), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
gsw: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x5F), range(0x61, 0x7D), 0xA0, 0xA9, 0xC4, 0xD6, 0xDC, 0xE4, 0xF6, 0xFC, 0x2011, 0x2019, 0x2030, 0x20AC, 0x2212],
fil: [range(0x20, 0x3F), range(0x41, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xD1, 0xF1, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
slk: [range(0x20, 0x21), range(0x24, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC1, 0xC4, 0xC9, 0xCD, range(0xD3, 0xD4), 0xDA, 0xDD, 0xE1, 0xE4, 0xE9, 0xED, range(0xF3, 0xF4), 0xFA, 0xFD, range(0x10C, 0x10F), range(0x139, 0x13A), range(0x13D, 0x13E), range(0x147, 0x148), range(0x154, 0x155), range(0x160, 0x161), range(0x164, 0x165), range(0x17D, 0x17E), 0x1C6, 0x1F3, range(0x2010, 0x2011), 0x2013, 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
ukr: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0x2BC, 0x404, range(0x406, 0x407), range(0x410, 0x429), 0x42C, range(0x42E, 0x449), 0x44C, range(0x44E, 0x44F), 0x454, range(0x456, 0x457), range(0x490, 0x491), 0x2011, 0x2013, 0x2019, 0x201C, 0x201E, 0x2030, 0x20AC, 0x2116],
fas: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x3A), range(0x3C, 0x3E), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, 0xAB, 0xBB, 0x609, 0x60C, 0x61B, 0x61F, range(0x621, 0x624), range(0x626, 0x63A), range(0x641, 0x642), range(0x644, 0x648), range(0x64B, 0x64D), 0x651, 0x654, range(0x66A, 0x66C), 0x67E, 0x686, 0x698, 0x6A9, 0x6AF, 0x6CC, range(0x6F0, 0x6F9), 0x200E, range(0x2010, 0x2011), 0x2026, 0x2030, range(0x2039, 0x203A), 0x20AC, 0x2212],
eus: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xC7, 0xD1, 0xE7, 0xF1, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC, 0x2212],
isl: [range(0x20, 0x5F), range(0x61, 0x62), range(0x64, 0x70), range(0x72, 0x76), range(0x78, 0x79), 0x7C, 0xA0, 0xA7, 0xA9, 0xC1, 0xC6, 0xC9, 0xCD, 0xD0, 0xD3, 0xD6, 0xDA, range(0xDD, 0xDE), 0xE1, 0xE6, 0xE9, 0xED, 0xF0, 0xF3, 0xF6, 0xFA, range(0xFD, 0xFE), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
tha: [range(0x20, 0x25), range(0x27, 0x3A), range(0x3C, 0x3E), 0x40, range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, range(0xE01, 0xE3A), range(0xE40, 0xE4E), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
vie: [range(0x20, 0x5F), range(0x61, 0x65), range(0x67, 0x69), range(0x6B, 0x76), range(0x78, 0x79), 0x7C, 0xA0, 0xA7, 0xA9, range(0xC0, 0xC3), range(0xC8, 0xCA), range(0xCC, 0xCD), range(0xD2, 0xD5), range(0xD9, 0xDA), 0xDD, range(0xE0, 0xE3), range(0xE8, 0xEA), range(0xEC, 0xED), range(0xF2, 0xF5), range(0xF9, 0xFA), 0xFD, range(0x102, 0x103), range(0x110, 0x111), range(0x128, 0x129), range(0x168, 0x169), range(0x1A0, 0x1A1), range(0x1AF, 0x1B0), range(0x1EA0, 0x1EF9), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
afr: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0xC1, 0xC2), range(0xC8, 0xCB), range(0xCE, 0xCF), 0xD4, 0xD6, 0xDB, range(0xE1, 0xE2), range(0xE8, 0xEB), range(0xEE, 0xEF), 0xF4, 0xF6, 0xFB, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
lit: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x50), range(0x52, 0x56), range(0x59, 0x5F), range(0x61, 0x70), range(0x72, 0x76), range(0x79, 0x7D), 0xA0, 0xA9, range(0x104, 0x105), range(0x10C, 0x10D), range(0x116, 0x119), range(0x12E, 0x12F), range(0x160, 0x161), range(0x16A, 0x16B), range(0x172, 0x173), range(0x17D, 0x17E), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC, 0x2212],
cym: [range(0x20, 0x5F), range(0x61, 0x6A), range(0x6C, 0x70), range(0x72, 0x75), 0x77, 0x79, 0x7C, 0xA0, 0xA7, 0xA9, range(0xC0, 0xC2), 0xC4, range(0xC8, 0xCF), range(0xD2, 0xD4), 0xD6, range(0xD9, 0xDD), range(0xE0, 0xE2), 0xE4, range(0xE8, 0xEF), range(0xF2, 0xF4), 0xF6, range(0xF9, 0xFD), 0xFF, range(0x174, 0x178), range(0x1E80, 0x1E85), range(0x1EF2, 0x1EF3), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
bul: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, range(0x410, 0x42A), 0x42C, range(0x42E, 0x44A), 0x44C, range(0x44E, 0x44F), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x2033, 0x20AC, 0x2116],
tel: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA9, range(0xC01, 0xC03), range(0xC05, 0xC0C), range(0xC0E, 0xC10), range(0xC12, 0xC28), range(0xC2A, 0xC33), range(0xC35, 0xC39), range(0xC3E, 0xC44), range(0xC46, 0xC48), range(0xC4A, 0xC4D), range(0xC55, 0xC56), range(0xC60, 0xC61), range(0xC66, 0xC6F), 0x2011, range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2030, 0x20AC],
glg: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, range(0xA0, 0xA1), 0xA7, 0xA9, 0xAB, 0xBB, 0xBF, 0xC1, 0xC9, 0xCD, 0xCF, 0xD1, 0xD3, 0xDA, 0xDC, 0xE1, 0xE9, 0xED, 0xEF, 0xF1, 0xF3, 0xFA, 0xFC, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
bre: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x50), range(0x52, 0x5F), range(0x61, 0x70), range(0x72, 0x7D), 0xA0, 0xA9, 0xCA, 0xD1, 0xD9, 0xEA, 0xF1, 0xF9, 0x2BC, 0x2011, 0x2030, 0x20AC],
mya: [0x20, range(0x23, 0x25), range(0x27, 0x39), range(0x3C, 0x3E), 0x40, range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA9, range(0x1000, 0x1021), range(0x1023, 0x1027), range(0x1029, 0x1032), range(0x1036, 0x104B), 0x104F, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, 0x20AC],
urd: [0x20, range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3A), range(0x3C, 0x3E), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, range(0x60C, 0x60D), 0x61B, 0x61F, 0x621, range(0x627, 0x628), range(0x62A, 0x63A), range(0x641, 0x642), range(0x644, 0x646), 0x648, range(0x66B, 0x66C), 0x679, 0x67E, 0x686, 0x688, 0x691, 0x698, 0x6A9, 0x6AF, 0x6BE, 0x6C1, 0x6CC, 0x6D2, 0x6D4, range(0x6F0, 0x6F9), 0x200E, 0x2011, 0x2030, 0x20AC],
bos: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x5F), range(0x61, 0x70), range(0x72, 0x76), 0x7A, 0x7C, 0xA0, 0xA9, range(0x106, 0x107), range(0x10C, 0x10D), range(0x110, 0x111), range(0x160, 0x161), range(0x17D, 0x17E), 0x1C4, range(0x1C6, 0x1C7), range(0x1C9, 0x1CA), 0x1CC, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
oci: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, range(0xC0, 0xC1), range(0xC7, 0xC9), 0xCD, 0xCF, range(0xD2, 0xD3), 0xDA, 0xDC, range(0xE0, 0xE1), range(0xE7, 0xE9), 0xED, 0xEF, range(0xF2, 0xF3), 0xFA, 0xFC, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2019, range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, 0x20AC, 0x22C5],
msa: [range(0x20, 0x5F), range(0x61, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
mal: [range(0x20, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, range(0xD02, 0xD03), range(0xD05, 0xD0C), range(0xD0E, 0xD10), range(0xD12, 0xD28), range(0xD2A, 0xD39), range(0xD3E, 0xD43), range(0xD46, 0xD48), range(0xD4A, 0xD4D), 0xD57, range(0xD60, 0xD61), range(0xD66, 0xD6F), range(0xD7A, 0xD7F), range(0x200C, 0x200D), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
bel: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA9, 0xAB, 0xBB, 0x401, 0x406, 0x40E, range(0x410, 0x417), range(0x419, 0x428), range(0x42B, 0x437), range(0x439, 0x448), range(0x44B, 0x44F), 0x451, 0x456, 0x45E, 0x2011, 0x2030, 0x20AC],
haw: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x5F), 0x61, 0x65, range(0x68, 0x69), range(0x6B, 0x70), 0x75, 0x77, range(0x7B, 0x7D), 0xA0, 0xA9, range(0x100, 0x101), range(0x112, 0x113), range(0x12A, 0x12B), range(0x14C, 0x14D), range(0x16A, 0x16B), 0x2BB, 0x2011, 0x2030, 0x20AC],
yid: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, 0x5BC, range(0x5BE, 0x5BF), 0x5C2, range(0x5D0, 0x5EA), range(0x5F3, 0x5F4), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2030, 0x20AC, 0xFB1D, 0xFB1F, 0xFB2B, range(0xFB2E, 0xFB2F), 0xFB35, 0xFB3B, 0xFB44, 0xFB4A, 0xFB4C, 0xFB4E],
asm: [range(0x20, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, 0x964, range(0x981, 0x983), range(0x985, 0x98B), range(0x98F, 0x990), range(0x993, 0x9A8), range(0x9AA, 0x9AF), 0x9B2, range(0x9B6, 0x9B9), 0x9BC, range(0x9BE, 0x9C3), range(0x9C7, 0x9C8), range(0x9CB, 0x9CE), range(0x9DC, 0x9DD), 0x9DF, range(0x9E6, 0x9F1), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
mar: [range(0x20, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA9, range(0x901, 0x903), range(0x905, 0x90D), range(0x90F, 0x911), range(0x913, 0x928), range(0x92A, 0x933), range(0x935, 0x939), range(0x93C, 0x943), 0x945, range(0x947, 0x949), range(0x94B, 0x94D), 0x950, range(0x966, 0x96F), 0x200D, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
gle: [range(0x20, 0x5F), range(0x61, 0x69), range(0x6C, 0x70), range(0x72, 0x75), 0x7C, 0xA0, 0xA7, 0xA9, 0xC1, 0xC9, 0xCD, 0xD3, 0xDA, 0xE1, 0xE9, 0xED, 0xF3, 0xFA, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
gla: [range(0x20, 0x49), range(0x4C, 0x50), range(0x52, 0x55), range(0x5B, 0x5F), range(0x61, 0x69), range(0x6C, 0x70), range(0x72, 0x75), range(0x7B, 0x7D), range(0xA0, 0xA1), 0xA7, 0xA9, 0xAE, 0xB0, range(0xB6, 0xB7), 0xC0, 0xC8, 0xCC, 0xD2, 0xD9, 0xE0, 0xE8, 0xEC, 0xF2, 0xF9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), range(0x2026, 0x2027), 0x2030, 0x204A, 0x20AC, 0x2122],
mkd: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA9, 0x403, 0x405, range(0x408, 0x40A), 0x40C, range(0x40F, 0x418), range(0x41A, 0x428), range(0x430, 0x438), range(0x43A, 0x448), 0x453, 0x455, range(0x458, 0x45A), 0x45C, 0x45F, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
nob: [range(0x20, 0x21), range(0x23, 0x25), range(0x27, 0x5F), range(0x61, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, range(0xBF, 0xC0), range(0xC5, 0xC6), 0xC9, range(0xD2, 0xD4), 0xD8, 0xE0, range(0xE5, 0xE6), 0xE9, range(0xF2, 0xF4), 0xF8, 0x2011, 0x2013, range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC, 0x2212],
mri: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), 0x41, 0x45, range(0x48, 0x49), 0x4B, range(0x4D, 0x50), 0x52, range(0x54, 0x55), 0x57, range(0x5B, 0x5F), 0x61, 0x65, range(0x67, 0x69), 0x6B, range(0x6D, 0x70), 0x72, range(0x74, 0x75), 0x77, range(0x7B, 0x7D), 0xA0, 0xA9, range(0x100, 0x101), range(0x112, 0x113), range(0x12A, 0x12B), range(0x14C, 0x14D), range(0x16A, 0x16B), 0x2011, 0x2030, 0x20AC],
san: [range(0x20, 0x40), range(0x5B, 0x60), range(0x7B, 0x7E), 0xA0, 0xA7, 0xA9, range(0x901, 0x903), range(0x905, 0x90C), range(0x90F, 0x910), range(0x913, 0x928), range(0x92A, 0x930), range(0x932, 0x933), range(0x935, 0x939), range(0x93C, 0x944), range(0x947, 0x948), range(0x94B, 0x94D), range(0x950, 0x952), range(0x960, 0x963), range(0x966, 0x96F), 0x2011, range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
zul: [range(0x20, 0x21), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x5F), range(0x61, 0x7D), 0xA0, 0xA9, 0x2011, 0x2030, 0x20AC],
ast: [range(0x20, 0x49), range(0x4C, 0x56), range(0x58, 0x5F), range(0x61, 0x69), range(0x6C, 0x76), range(0x78, 0x7A), 0x7C, range(0xA0, 0xA1), 0xA7, 0xA9, 0xAB, 0xBB, 0xBF, 0xC1, 0xC9, 0xCD, 0xD1, 0xD3, 0xDA, 0xDC, 0xE1, 0xE9, 0xED, 0xF1, 0xF3, 0xFA, 0xFC, range(0x1E24, 0x1E25), range(0x1E36, 0x1E37), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
swa: [range(0x20, 0x22), range(0x24, 0x25), range(0x27, 0x29), range(0x2B, 0x3F), range(0x41, 0x50), range(0x52, 0x57), range(0x59, 0x5F), range(0x61, 0x70), range(0x72, 0x77), range(0x79, 0x7D), 0xA0, 0xA9, 0x2011, 0x2030, 0x20AC],
kat: [range(0x20, 0x21), range(0x23, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, range(0x10D0, 0x10F0), 0x10FB, range(0x1C90, 0x1CB0), range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC, 0x2116],
hye: [0x20, range(0x24, 0x25), 0x27, range(0x2B, 0x3A), range(0x3C, 0x3E), 0x5C, range(0x5E, 0x5F), 0x7C, 0xA0, 0xA9, 0xAB, 0xBB, range(0x531, 0x556), range(0x55A, 0x55F), range(0x561, 0x586), 0x58A, 0x2011, 0x2030, 0x20AC],
kaz: [range(0x20, 0x40), range(0x5B, 0x5F), range(0x7B, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0x401, 0x406, range(0x410, 0x44F), 0x451, 0x456, range(0x492, 0x493), range(0x49A, 0x49B), range(0x4A2, 0x4A3), range(0x4AE, 0x4B1), range(0x4BA, 0x4BB), range(0x4D8, 0x4D9), range(0x4E8, 0x4E9), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), 0x2026, 0x2030, 0x20AC],
tuk: [range(0x20, 0x25), range(0x27, 0x42), range(0x44, 0x50), range(0x52, 0x55), 0x57, range(0x59, 0x5F), range(0x61, 0x62), range(0x64, 0x70), range(0x72, 0x75), 0x77, range(0x79, 0x7D), 0xA0, 0xA7, 0xA9, 0xC4, 0xC7, 0xD6, range(0xDC, 0xDD), 0xE4, 0xE7, 0xF6, range(0xFC, 0xFD), range(0x147, 0x148), range(0x15E, 0x15F), range(0x17D, 0x17E), 0x2011, range(0x2013, 0x2014), range(0x201C, 0x201D), 0x2026, 0x2030, 0x20AC],
uzb: [range(0x20, 0x56), range(0x58, 0x5F), range(0x61, 0x76), range(0x78, 0x7A), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2BB, 0x2BC), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
ltz: [range(0x20, 0x5F), range(0x61, 0x7D), 0xA0, 0xA7, 0xA9, 0xAB, 0xBB, 0xC4, 0xC9, 0xCB, 0xE4, 0xE9, 0xEB, range(0x2010, 0x2011), range(0x2013, 0x2014), 0x2018, 0x201A, 0x201C, 0x201E, 0x2026, 0x2030, 0x20AC],
mon: [range(0x20, 0x40), range(0x5B, 0x5F), 0x7C, 0xA0, 0xA7, 0xA9, 0x401, range(0x410, 0x44F), 0x451, range(0x4AE, 0x4AF), range(0x4E8, 0x4E9), range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
som: [range(0x20, 0x40), range(0x42, 0x44), range(0x46, 0x48), range(0x4A, 0x4E), range(0x51, 0x54), range(0x57, 0x59), range(0x5B, 0x5F), range(0x62, 0x64), range(0x66, 0x68), range(0x6A, 0x6E), range(0x71, 0x74), range(0x77, 0x79), 0x7C, 0xA0, 0xA7, 0xA9, range(0x2010, 0x2011), range(0x2013, 0x2014), range(0x2018, 0x2019), range(0x201C, 0x201D), range(0x2020, 0x2021), 0x2026, 0x2030, range(0x2032, 0x2033), 0x20AC],
});
const detectScript = literals => detectAlphabet(literals, {
Latn: [range(0x20, 0x7E), range(0xA0, 0xFF), range(0x100, 0x17F), range(0x180, 0x24F), range(0x250, 0x2AF), range(0x2B0, 0x2FF), range(0x300, 0x36F), range(0x1E00, 0x1EFF), range(0x2C60, 0x2C7F), range(0xA720, 0xA7FF), range(0xAB30, 0xAB6F), range(0x10780, 0x107BF), range(0x1DF00, 0x1DFFF)],
Cyrl: [range(0x400, 0x4FF), range(0x500, 0x52F), range(0x2DE0, 0x2DFF), range(0xA640, 0xA69F), range(0x1C80, 0x1C8F), range(0x1E030, 0x1E08F)],
});
const languageIdentifier = text => globalXHR('https://api.translatedlabs.com/language-identifier/identify', {
responseType: 'json',
}, { text: text, etnologue: true, uiLanguage: 'en' }).then(function({response}) {
console.log('Online language identifier success:', response);
let iso6393 = /^(\w{2})-(\w{2})$/.exec(response.code);
return iso6393 != null && (iso6393 = {
aa: 'aar', ab: 'abk', ae: 'ave', af: 'afr', ak: 'aka', am: 'amh', an: 'arg', ar: 'ara', as: 'asm',
av: 'ava', ay: 'aym', az: 'aze', ba: 'bak', be: 'bel', bg: 'bul', bi: 'bis', bm: 'bam', bn: 'ben',
bo: 'bod', br: 'bre', bs: 'bos', ca: 'cat', ce: 'che', ch: 'cha', co: 'cos', cr: 'cre', cs: 'ces',
cu: 'chu', cv: 'chv', cy: 'cym', da: 'dan', de: 'deu', dv: 'div', dz: 'dzo', ee: 'ewe', el: 'ell',
en: 'eng', eo: 'epo', es: 'spa', et: 'est', eu: 'eus', fa: 'fas', ff: 'ful', fi: 'fin', fj: 'fij',
fo: 'fao', fr: 'fra', fy: 'fry', ga: 'gle', gd: 'gla', gl: 'glg', gn: 'grn', gu: 'guj', gv: 'glv',
ha: 'hau', he: 'heb', hi: 'hin', ho: 'hmo', hr: 'hrv', ht: 'hat', hu: 'hun', hy: 'hye', hz: 'her',
ia: 'ina', id: 'ind', ie: 'ile', ig: 'ibo', ii: 'iii', ik: 'ipk', io: 'ido', is: 'isl', it: 'ita',
iu: 'iku', ja: 'jpn', jv: 'jav', ka: 'kat', kg: 'kon', ki: 'kik', kj: 'kua', kk: 'kaz', kl: 'kal',
km: 'khm', kn: 'kan', ko: 'kor', kr: 'kau', ks: 'kas', ku: 'kur', kv: 'kom', kw: 'cor', ky: 'kir',
la: 'lat', lb: 'ltz', lg: 'lug', li: 'lim', ln: 'lin', lo: 'lao', lt: 'lit', lu: 'lub', lv: 'lav',
mg: 'mlg', mh: 'mah', mi: 'mri', mk: 'mkd', ml: 'mal', mn: 'mon', mr: 'mar', ms: 'msa', mt: 'mlt',
my: 'mya', na: 'nau', nb: 'nob', nd: 'nde', ne: 'nep', ng: 'ndo', nl: 'nld', nn: 'nno', no: 'nor',
nr: 'nbl', nv: 'nav', ny: 'nya', oc: 'oci', oj: 'oji', om: 'orm', or: 'ori', os: 'oss', pa: 'pan',
pi: 'pli', pl: 'pol', ps: 'pus', pt: 'por', qu: 'que', rm: 'roh', rn: 'run', ro: 'ron', ru: 'rus',
rw: 'kin', sa: 'san', sc: 'srd', sd: 'snd', se: 'sme', sg: 'sag', si: 'sin', sk: 'slk', sl: 'slv',
sm: 'smo', sn: 'sna', so: 'som', sq: 'sqi', sr: 'srp', ss: 'ssw', st: 'sot', su: 'sun', sv: 'swe',
sw: 'swa', ta: 'tam', te: 'tel', tg: 'tgk', th: 'tha', ti: 'tir', tk: 'tuk', tl: 'tgl', tn: 'tsn',
to: 'ton', tr: 'tur', ts: 'tso', tt: 'tat', tw: 'twi', ty: 'tah', ug: 'uig', uk: 'ukr', ur: 'urd',
uz: 'uzb', ve: 'ven', vi: 'vie', vo: 'vol', wa: 'wln', wo: 'wol', xh: 'xho', yi: 'yid', yo: 'yor',
za: 'zha', zh: 'zho', zu: 'zul',
}[iso6393[1].toLowerCase()]) ? Object.assign(response, { iso6393: iso6393 })
: Promise.reject('Language not resolved');
});
const labelMapper = label => dashUnifier(label);
if (editionInfo != null) {
if (incompleteEdition.test(editionInfo.lastChild.textContent.trim()))
editionInfo.parentNode.style.backgroundColor = '#f001';
if (editionSearch && editionInfo.parentNode.querySelector('span[class$="-edition-search"]') == null) {
function addSearch(className, resourceName, callBack, title) {
const span = document.createElement('span'), defaultOpacity = 0.5;
[span.style, span.className, span.innerHTML] =
['float: right; margin-left: 3pt;', className, GM_getResourceText(resourceName)];
const icon = span.querySelector('svg');
if (icon == null) throw 'Assertion failed: no SVG in resource';
icon.setAttribute('height', '1em');
icon.removeAttribute('width');
[icon.style.cursor, icon.style.opacity, icon.style.transition] =
['pointer', defaultOpacity, 'opacity 100ms, scale 100ms'];
icon.dataset.torrentId = torrentId;
icon.onclick = function(evt) {
const target = evt.currentTarget, editionTR = target.closest('tr.edition');
if (editionTR == null) throw 'Assertion failed: edition row not found';
if (target.disabled) return; else target.disabled = true;
const haveResults = Boolean(eval(target.dataset.haveResults));
const animation = haveResults ? null : flashElement(target);
queryAjaxAPICached('torrent', { id: target.dataset.torrentId }).then(function(torrent) {
console.assert(torrent.torrent.remasterYear > 0);
const [labels, catNos] = editionInfoParser(torrent.torrent);
const roleMapper = importance => torrent.group.musicInfo && (importance = torrent.group.musicInfo[importance])
&& importance.length > 0 ? importance.map(artist => artist.name) : undefined;
const artists = torrent.group.releaseType == 7 ? 'Various Artists'
: roleMapper('dj') || roleMapper('artists');
const nonemptyArray = array => array.length > 0 ? array : undefined;
const searchParams = {
artists: artists,
releaseTitle: torrent.group.name ? releaseTitleNorm(torrent.group.name) : undefined,
year: torrent.torrent.remasterYear > 0 ? torrent.torrent.remasterYear : undefined,
labels: nonemptyArray(labels),
searchLabels: nonemptyArray(labels.filter(label => !rxNoLabel.test(label))),
catNos: nonemptyArray(catNos),
searchCatNos: nonemptyArray(decodeHTML(torrent.torrent.remasterCatalogueNumber).split(rxEditionSplitter)
.map(value => value.trim()).map(catno => !rxNoCatno.test(catno) ? catno.replace(rxCatNoRange, '$1$2') : undefined)
.filter((s1, n, a) => s1 && a.findIndex(s2 => s2.toLowerCase() == s1.toLowerCase()) == n)),
barcodes: nonemptyArray(catNos.map(catNo => (catNo = catNo.replace(/\D+/g, '')).length >= 10
&& catNo.length <= 13 ? catNo : null).filter(Boolean)),
editionTitle: torrent.torrent.remasterTitle || undefined,
releaseType: torrent.group.releaseType,
};
return searchParams.searchCatNos && (searchParams.searchLabels || searchParams.year)
|| searchParams.barcodes || searchParams.releaseTitle && (searchParams.artists || searchParams.year) ?
callBack(searchParams, haveResults) : null;
}, alert).catch(function(reason) {
target.style.fill = 'red';
for (let def of target.getElementsByTagName('defs')) def.remove();
for (let path of target.getElementsByTagName('path')) {
path.removeAttribute('fill');
path.style.fill = null;
}
target.insertAdjacentHTML('afterbegin', '<title>' + reason + '</title>');
return false;
}).then(function(results) {
if (results !== undefined) target.dataset.haveResults = true;
if (results === null) return target.parentNode.remove();
if (results instanceof HTMLElement) editionTR.after(results);
if (animation != null) animation.cancel();
[target.style.opacity, target.disabled] = [1, false];
});
};
icon.onmouseenter = span.firstElementChild.onmouseleave = function(evt) {
if (evt.type == 'mouseenter') {
evt.currentTarget.style.opacity = 1;
evt.currentTarget.style.scale = 1.25;
} else {
if (!evt.currentTarget.dataset.haveResults) evt.currentTarget.style.opacity = defaultOpacity;
evt.currentTarget.style.scale = 'none';
}
};
svgSetTitle(icon, title);
editionInfo.after(span);
}
addSearch('discogs-edition-search', 'dc_icon', function(params, haveResults) {
function openInNewWindow(background = false) {
const url = new URL('search', dcOrigin);
url.searchParams.set('type', 'release');
if (params.searchLabels) url.searchParams.set('label', params.searchLabels.join(' '));
if (params.searchCatNos) url.searchParams.set('catno', params.searchCatNos.join(' '));
if (!params.searchCatNos || !params.searchLabels) {
if (params.year) url.searchParams.set('year', params.year);
if (params.releaseTitle) url.searchParams.set('title', params.releaseTitle);
if (!params.releaseTitle || !params.year)
if (Array.isArray(params.artists)) url.searchParams.set('artist', params.artists[0]);
else if (params.artists == 'Various Artists') url.searchParams.set('format', 'Compilation');
}
//url.searchParams.set('format', 'CD');
GM_openInTab(url.href, background);
if (!params.barcodes) return;
url.searchParams.delete('label'); url.searchParams.delete('catno');
for (let barcode of params.barcodes) {
url.searchParams.set('barcode', barcode);
GM_openInTab(url.href, background);
}
}
if (!dcAuth && !params.searchLabels && !params.searchCatNos) return Promise.resolve(null);
if (autoOpenTab || haveResults || !dcAuth) openInNewWindow(!haveResults);
if (haveResults || !dcAuth) return Promise.resolve(undefined);
const searchMethods = { };
if (params.searchCatNos || params.barcodes) {
const searches = [ ], search = { };
if (params.searchLabels) search.label = params.searchLabels.join(' ');
if (params.searchCatNos) search.catno = params.searchCatNos.join(' ');
if ((!params.searchLabels || !params.searchCatNos) && params.year) search.year = params.year;
if (Object.keys(search).length > 0) searches.push(search);
if (params.barcodes)
Array.prototype.push.apply(searches, params.barcodes.map(barcode => ({ barcode: barcode })));
if (searches.length > 0) searchMethods['label / cat# / barcode'] = searches;
}
if (params.searchLabels && params.searchCatNos
&& Math.min(...['Labels', 'CatNos'].map(p => params['search' + p].length)) > 2
&& params.searchLabels.length * params.searchCatNos.length <= 15)
searchMethods['single label / cat#'] = Array.prototype.concat.apply([ ], params.searchLabels.map(label =>
params.searchCatNos.map(catNo => ({ label: label, catno: catNo }))));
if (params.releaseTitle) {
const releaseTitles = parseLanguages(params.releaseTitle);
if (Array.isArray(params.artists)) searchMethods['artist / album'] = Array.prototype.concat.apply([ ],
params.artists.map(artist => Array.prototype.concat.apply([ ], parseLanguages(artist, true).map(artist =>
releaseTitles.map(releaseTitle => ({ release_title: releaseTitle, artist: artist }))))));
if (params.year) searchMethods['album / year'] =
releaseTitles.map(releaseTitle => ({ release_title: releaseTitle, year: params.year }));
}
return Object.keys(searchMethods).length > 0 ? (function searchMethod(index = 0) {
return index < Object.keys(searchMethods).length ? (searches =>
Promise.all(searches.map(search => (function searchPage(page = 1) {
return dcApiRequest('database/search', Object.assign({
type: 'release', sort: 'score',
page: page, per_page: 100,
}, search)).then(function(response) {
return parseInt(response.pagination.pages) > parseInt(response.pagination.page) ?
searchPage(page + 1).then(Array.prototype.concat.bind(response.results)) : response.results;
}, function(reason) {
console.warn(reason);
return [ ];
});
})())).then(results => Array.prototype.concat.apply([ ], results).filter((result, index, array) =>
array.findIndex(result2 => result2.id == result.id) == index)).then(function(results) {
const resultsFilter = (matchYear = true) => (matchYear = results.filter(function(release) {
if (matchYear && release.year && parseInt(release.year) != params.year) return false;
return !release.formats || release.formats.some(isDiscogsCD);
})).length > 0 ? matchYear : false;
return resultsFilter(true) || resultsFilter(false) || Promise.reject('Not found by any method');
}))(searchMethods[Object.keys(searchMethods)[index]]).then(results => [results, index],
reason => searchMethod(index + 1)) : Promise.reject('Nothing found by any method');
})().then(function([results, methodIndex]) {
const [tr, td, table, thead, tbody] = createElements('tr', 'td', 'table', 'div', 'tbody');
[tr.className, td.colSpan, thead.style] = ['edition-search-results', 6, theadStyle];
thead.innerHTML = `<b>Matched by ${Object.keys(searchMethods)[methodIndex]}</b> (${results.length})`;
const rowWorkers = [ ];
results.forEach(function(release, index) {
const [tr, artist, title, mediaFormats, releaseEvents, companies, catNos, barcode, groupSize] =
createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td', 'td', 'td');
tr.className = 'discogs-release';
[barcode.style, groupSize.style.textAlign] = ['white-space: break-spaces; max-width: 30%;', 'right'];
title.innerHTML = linkHTML(dcOrigin + release.uri, release.title, 'discogs-release');
let descriptors = getDiscogsReleaseDescriptors(release);
if (descriptors.length > 0) appendDisambiguation(title, descriptors.join(', '));
if (release.thumb || release.cover_image) addThumbnail(title, release.thumb || release.cover_image,
[dcOrigin, 'release', release.id, 'images'].join('/'));
if (release.formats) mediaFormats.innerHTML = release.formats.filter(format =>
parseInt(format.qty) > 0).map(function(format) {
const medium = format.qty + '×' + format.name, descriptors = getFormatDescriptions(format);
return /*descriptors.length > 0 ? medium + ' (' + descriptors.join(', ') + ')' : */medium;
}).join('<br>');
if (release.country || release.year) fillListRows(releaseEvents,
iso3166ToFlagCodes(discogsCountryToIso3166Mapper(release.country)).map(countryCode =>
releaseEventMapper(countryCode, release.year, params.year)), 3);
if (Array.isArray(release.label)) fillListRows(companies, release.label.map(stripDiscogsNameVersion)
.filter(uniqueValues).map(label => editionInfoMapper(label, undefined, params.labels)), 3, true);
if (release.catno) fillListRows(catNos, [editionInfoMapper(undefined, release.catno, null, params.catNos)]);
const barcodes = (release.barcode || [ ]).map(function(barcode) {
const div = Object.assign(document.createElement('div'), {
textContent: barcode,
className: 'identifier',
});
if (params.catNos && params.catNos.some(catNo => sameBarcodes(catNo, barcode)))
editionInfoMatchingStyle(div);
return div;
});
if (barcodes.length > 0); barcode.append(...barcodes);
rowWorkers.push(dcApiRequest('releases/' + release.id).catch(reason =>
globalXHR([dcOrigin, 'release', release.id].join('/'), { method: 'HEAD' }).then(function({finalUrl}) {
const releaseId = discogsIdExtractor(finalUrl, 'release');
return releaseId > 0 && releaseId != release.id ?
dcApiRequest('releases/' + releaseId) : Promise.reject(reason);
}, () => Promise.reject(reason))).then(function(release) {
const releaseLink = title.querySelector('a.discogs-release');
if (releaseLink != null) [releaseLink.href/*, releaseLink.textContent*/] = [release.uri, release.title];
// setDiscogsArtist(artist, release.artists);
if (release.labels.length <= 0) return;
while (catNos.lastChild != null) catNos.removeChild(catNos.lastChild);
fillListRows(catNos, release.labels.map(label => label.catno).filter(uniqueValues)
.map(catNo => editionInfoMapper(undefined, catNo, null, params.catNos)));
setDiscogsTooltip(release, tr);
}, function(reason) {
tr.style = 'background-color: #f001; opacity: 0.4;';
setTooltip(tr, reason);
}));
setDiscogsGroupSize(release, groupSize);
tr.append(title, mediaFormats, releaseEvents, companies, catNos, barcode, groupSize);
for (let cell of tr.cells) cell.style.backgroundColor = 'inherit';
['title', 'media-formats', 'release-events', 'labels-and-companies', 'cat-nos', 'barcode', 'editions-total']
.forEach((className, index) => { tr.cells[index].className = className });
tbody.append(tr);
});
table.append(thead, tbody); td.append(thead, table); tr.append(td);
Promise.all(rowWorkers).then(() => { addResultsFilter(thead, tbody, 5) }, console.warn);
return tr;
}) : Promise.resolve(null);
}, 'Search edition on Discogs\n(Discogs API authorization required for embedded results)');
addSearch('musicbrainz-edition-search', 'mb_logo', function(params, haveResults) {
const queries = [ ], yearField = 'date:' + params.year;
if (params.searchLabels && params.searchCatNos
|| params.year > 0 && (/*params.searchLabels || */params.searchCatNos)) {
const fields = [ ];
if (params.searchLabels)
fields.push(`(${params.searchLabels.map(label => 'label:"' + label + '"').join(' OR ')})`);
if (params.searchCatNos || params.barcodes) fields.push(`(${(params.searchCatNos || [ ])
.map(catNo => 'catno:"' + catNo + '"').concat((params.barcodes || [ ])
.map(catNo => 'barcode:"' + catNo + '"')).join(' OR ')})`);
fields.push(params.searchLabels && params.searchCatNos ? `(${yearField} OR (NOT date:*))` : yearField);
queries.push(fields);
}
if (params.releaseTitle) {
const release = `(${parseLanguages(params.releaseTitle).map(release => '(' + [
`release:"${release}"`,
`alias:"${release}"`,
].join(' OR ') + ')').join(' OR ')})`;
const artistMapper = artist =>
`(${parseLanguages(artist, true).map(artist => `artistname:"${artist}" OR creditname:"${artist}"`).join(' OR ')})`;
if (params.year > 0) queries.push(Array.isArray(params.artists) && ![7, 19].includes(params.releaseType) ? [
release,
'(' + params.artists.map(artistMapper).join(' OR ') + ')',
`(${yearField} OR (NOT date:*))`,
] : [release, yearField]);
if (params.releaseType == 7) queries.push([release, 'secondarytype:compilation']);
else if (params.releaseType == 19) queries.push([release, 'secondarytype:DJ-mix']);
else if (Array.isArray(params.artists) && params.artists.length > 0)
queries.push([release, params.artists.map(artistMapper).join(' AND ')]);
}
const formats = '(' + [
'CD', 'CD-R', 'Enhanced CD', 'SHM-CD', 'Blu-spec CD', 'Copy Control CD',
'HDCD', '8cm CD', 'Hybrid SACD (CD layer)', 'DualDisc (CD side)', 'DualDisc',
'Hybrid SACD', 'DTS CD', 'HQCD', 'CD+G', '8cm CD+G', 'Mixed Mode CD', 'Minimax CD',
].map(format => 'format:"' + format + '"').join(' OR ') + ' OR (NOT format:*))';
if (queries.length <= 0) return Promise.resolve(null);
if (autoOpenTab || haveResults) {
const url = new URL('search', mbOrigin);
url.searchParams.set('method', 'advanced');
url.searchParams.set('type', 'release');
url.searchParams.set('query', '(' + queries.map(query => query.join(' AND '))
.map(query => '(' + query + ')').join(' OR ') + ') AND ' + formats);
GM_openInTab(url.href, !haveResults);
}
return haveResults ? Promise.resolve(undefined) : (function doQuery(index = 0) {
return index < queries.length ? mbApiRequest('release', {
query: queries[index].concat(formats).join(' AND '),
limit: 100,
}).then(({releases}) => releases.length > 0 ? releases : doQuery(index + 1))
: Promise.reject('Nothing found by any method');
})().then(function(releases) {
const [tr, td, table, thead, tbody] = createElements('tr', 'td', 'table', 'div', 'tbody');
[tr.className, td.colSpan, thead.innerHTML, thead.style, thead.style.minHeight] =
['edition-search-results', 6, '<b>MusicBrainz</b>', theadStyle, '1em'];
releases.forEach(function(release, index) {
const [tr, artist, title, releaseEvents, labels, catNos, barcode, groupSize, releasesWithId] =
createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td', 'td', 'td');
tr.className = 'musicbrainz-release';
if (release.quality == 'low') tr.style.opacity = 0.75;
[releaseEvents, barcode].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[groupSize, releasesWithId].forEach(elem => { elem.style.textAlign = 'right' });
setMusicBrainzArtist(release, artist, false);
setMusicBrainzTitle(release, title);
setMusicBrainzReleaseEvents(release, releaseEvents, params.year);
if (Array.isArray(release['label-info'])) {
fillListRows(labels, release['label-info'].map(labelInfo =>
labelInfo.label && labelInfo.label.name).filter(uniqueValues).map(label =>
editionInfoMapper(label, undefined, params.labels)));
fillListRows(catNos, release['label-info'].map(labelInfo => labelInfo['catalog-number'])
.filter(uniqueValues).map(catNo => editionInfoMapper(undefined, catNo, undefined, params.catNos)));
}
if (release.barcode) {
barcode.textContent = release.barcode;
if (params.barcodes && params.barcodes.some(catalogueNumber =>
sameBarcodes(catalogueNumber, release.barcode))) editionInfoMatchingStyle(barcode);
}
setMusicBrainzGroupSize(release, groupSize, releasesWithId);
setMusicBrainzTooltip(release, tr);
tr.append(artist, title, releaseEvents, labels, catNos, barcode, groupSize, releasesWithId);
for (let cell of tr.cells) cell.style.fontSize = cell.style.backgroundColor = 'inherit';
['artist', 'title', 'release-events', 'labels', 'cat-nos', 'barcode', 'editions-total', 'discids-total']
.forEach((className, index) => { tr.cells[index].className = className });
tbody.append(tr);
});
table.append(thead, tbody); td.append(thead, table); tr.append(td);
addResultsFilter(thead, tbody, 5);
return tr;
});
}, 'Search edition on MusicBrainz');
addSearch('allmusic-edition-search', 'am_logo', function(params, haveResults) {
if (!params.releaseTitle) return Promise.reject('Insufficient parameters');
const searchTerms = [params.releaseTitle];
if (params.releaseType == 7) searchTerms.unshift('Various');
else if (Array.isArray(params.artists))
Array.prototype.unshift.apply(searchTerms, params.artists.slice(0, 3));
const origin = 'https://www.allmusic.com', searchLink = origin + '/search/albums/' +
encodeURIComponent(searchTerms.map(searchTerm => '"' + searchTerm + '"').join(' '));
if (autoOpenTab || haveResults) GM_openInTab(searchLink, !haveResults);
if (haveResults) return Promise.resolve(undefined);
return globalXHR(searchLink).then(({document}) => Promise.all(Array.from(document.body.querySelectorAll('div#resultsContainer div.album, div#resultsContainer div.song'), function(div) {
function urlResolver(elem) {
if (elem instanceof HTMLAnchorElement) try { return new URL(elem.getAttribute('href'), origin).href }
catch(e) {
console.warn(e);
return elem.href;
}
}
const textMapper = elem => elem instanceof HTMLElement && elem.textContent.trim() || undefined;
const yearMapper = elem => (elem = textMapper(elem)) && parseInt(elem) || undefined;
const linkMapper = a => a instanceof HTMLAnchorElement ? {
text: a.textContent.trim(),
url: urlResolver(a),
} : undefined;
const coverMapper = img => img instanceof HTMLImageElement ? (function(param) {
if (param) try {
if (!(param = new URL(param)).pathname.includes('/images/no_image/')) return param.href;
} catch(e) { console.warn(e) }
})(img.src || img.dataset.src) : undefined;
const album = {
type: 'album',
artist: linkMapper(div.querySelector('div.artist > a')),
title: textMapper(div.querySelector('div.title > a')),
year: yearMapper(div.querySelector('div.year')),
genres: textMapper(div.querySelector('div.genres')),
url: urlResolver(div.querySelector('div.title > a')),
cover: coverMapper(div.querySelector('div.cover img')),
};
if (album.genres) album.genres = album.genres.split(',').map(genre => genre.trim());
return album.url ? globalXHR(album.url + '/releasesAjax', { headers: { Referer: album.url } }).then(function({document}) {
let releases = Array.from(document.body.querySelectorAll('table.releaseTable > tbody > tr'), function(tr) {
const release = {
type: 'release',
title: textMapper(tr.querySelector('span.title > a')) || album.title,
url: urlResolver(tr.querySelector('span.title > a')),
cover: coverMapper(tr.querySelector('td.cover img')),
};
let elem = tr.querySelector('td.yearFormat');
if (elem != null) [release.year, release.format] =
[yearMapper(elem.children[0]), textMapper(elem.children[1])];
if ((elem = tr.querySelector('span.labelRelId')) != null) {
const labels = elem.getElementsByTagName('a');
console.assert(labels.length < 2, elem);
if (labels.length > 0) release.label = textMapper(labels[0]);
if (elem.lastChild.nodeType == Node.TEXT_NODE)
release.catNo = elem.lastChild.textContent.trim().replace(/^-\s*/, '');
}
return release;
});
releases = releases.filter(release => !release.format || ['CD', 'CD-R'].includes(release.format));
return releases.length > 0 ? Promise.all(releases.map(function(release) {
release = Object.assign({ }, album, release);
return release.url ? globalXHR(release.url + '/trackListingAjax', { headers: { Referer: release.url } }).then(function({document}) {
if (!(document instanceof HTMLDocument)) return Object.assign(release, { discs: null });
const discs = document.body.querySelectorAll('div#trackContainer > div.disc');
return Object.assign(release, {
discs: Array.from(discs, disc => disc.querySelectorAll(':scope > div.track').length),
});
}, function(reason) {
console.warn(reason);
return release;
}) : release;
})) : album;
}, reason => album) : album;
}))).then(function(results) {
function addResult(result) {
function setArtist(...artists) {
if (artists.length > 0) artists.forEach(function(amArtist, index, artists) {
if (index > 0) artist.append(index < artists.length - 1 ? ', ' : ' & ');
artist.append(Object.assign(document.createElement('a'), {
href: amArtist.url,
target: '_blank',
style: noLinkDecoration,
textContent: amArtist.text,
className: 'allmusic-artist',
}));
});
}
const [tr, artist, title, releaseEvent, format, editionInfo, discs] = createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td');
discs.style.textAlign = 'right';
if (result.artist) setArtist(result.artist);
if (result.title) title.innerHTML = linkHTML(result.type == 'album' ? result.url + '#releases'
: result.url, result.title, 'allmusic-' + result.type);
if (result.cover) addThumbnail(title, result.cover);
if (result.year > 0) releaseEvent.append(...releaseEventMapper(undefined, result.year, params.year));
if (result.type == 'release') {
if (result.format) format.textContent = result.format;
fillListRows(editionInfo, [editionInfoMapper(result.label, result.catNo, params.labels, params.catNos)]);
discs.textContent = result.discs ? result.discs.join('+') : '−';
discs.title = 'Track counts';
if (!result.discs || !result.discs.some(disc => disc > 0)) tr.style.opacity = 0.75;
}
tr.className = 'allmusic-' + result.type;
tr.append(artist, artist, title, releaseEvent, /*format, */editionInfo, discs);
['artist', 'title', 'release-event', /*'format', */'edition-info', 'discs']
.forEach((className, index) => { tr.cells[index].className = className });
if (result.type == 'album') tr.style.fontStyle = 'italic';
tbody.append(tr);
}
if (results.length <= 0) return Promise.reject('Nothing found');
const [tr, td, table, thead, tbody] = createElements('tr', 'td', 'table', 'div', 'tbody');
[tr.className, td.colSpan, thead.innerHTML, thead.style, thead.style.minHeight] =
['edition-search-results', 6, '<b>AllMusic</b>', theadStyle, '1em'];
for (let result of results) if (Array.isArray(result)) result.forEach(addResult); else addResult(result);
table.append(thead, tbody); td.append(thead, table); tr.append(td);
addResultsFilter(thead, tbody, 5);
return tr;
});
}, 'Search release on AllMusic');
}
}
torrentDetails.dataset.torrentId = torrentId;
const useCountryInTitle = GM_getValue('use_country_in_title', true);
if (edition > 0) torrentDetails.dataset.editionGroup = edition;
const container = document.createElement('span');
container.style = 'display: inline-flex; flex-flow: row nowrap; column-gap: 2pt; justify-content: space-around;';
linkBox.append(' ', container);
const svgBulletHTML = color => `<svg height="0.8em" style="margin-right: 3pt;" viewBox="0 0 10 10"><circle fill="${color || ''}" cx="5" cy="5" r="5"></circle></svg>`;
addLookup(svgCaption('mb_text', 'filter: saturate(30%) brightness(130%);', 'MusicBrainz'), function(evt) {
function logScoreTest(testFn) {
if (typeof testFn != 'function') throw 'Invalid argument';
return logScoresCache && Array.isArray(logScoresCache[torrentId]) && logScoresCache[torrentId].some(testFn);
}
const target = evt.currentTarget;
console.assert(target instanceof HTMLElement);
const torrentId = parseInt(torrentDetails.dataset.torrentId);
console.assert(torrentId > 0);
if (evt.altKey) { // alternate lookup by CDDB ID
if (target.disabled) return; else target.disabled = true;
lookupByToc(torrentId, (tocEntries, discNdx, totalDiscs) => Promise.resolve(getCDDBiD(tocEntries))).then(function(discIds) {
for (let discId of Array.from(discIds).reverse()) if (discId != null)
GM_openInTab([mbOrigin, 'otherlookup', 'freedbid?other-lookup.freedbid=' + discId].join('/'), false);
}).catch(reason => { [target.textContent, target.style.color] = [reason, 'red'] }).then(() => { target.disabled = false });
} else if (Boolean(target.dataset.haveResponse)) {
if ('releaseIds' in target.dataset) for (let id of JSON.parse(target.dataset.releaseIds).reverse())
GM_openInTab([mbOrigin, 'release', id].join('/'), false);
// GM_openInTab([mbOrigin, 'cdtoc', evt.shiftKey ? 'attach?toc=' + JSON.parse(target.dataset.toc).join(' ')
// : target.dataset.discId].join('/'), false);
} else {
function gidFromResponse({document}) {
for (let gid of document.body.querySelectorAll('script[type="application/json"]')) try {
if ((gid = JSON.parse(gid.text).entity) && (gid = mbIdExtractor(gid.gid))) return gid;
} catch(e) { console.warn(e, gid) }
return Promise.reject('Incorrect response structure');
}
const taskAttribute = task => task ? {
id: '39867b3b-0f1e-40d5-b602-4f3936b7f486',
value: task.replace(...untitleCase),
} : null;
function guessSPA(name) {
const patterns = {
[mb.spa.theatre]: /\b(?:Cast)\b/i, [mb.spa.disney]: /\b(?:Disney)\b/i,
// [mb.spa.data]: /\b(?:)\b/i,
// [mb.spa.churchChimes]: /\b(?:)\b/i,
// [mb.spa.languageInstruction]: /\b(?:)\b/i,
};
for (let mbid in patterns) if (patterns[mbid].test(name)) return Promise.resolve(mbid);
return Promise.reject('Name does not look like known SPA');
}
function getTrackLength(track) {
if (!track) throw 'Invalid argument';
let trackLength = track.length || track.duration;
if (!trackLength) return;
if (/^(\d+)$/.test(trackLength)) trackLength = parseInt(trackLength);
else if ((trackLength = /^(?:(\d+):)?(\d+):(\d+)$/.exec(trackLength)) != null)
trackLength = trackLength.slice(1).reverse().reduce((s, t, n) => s + (parseInt(t) || 0) * 60**n, 0) * 1000;
console.assert(trackLength > 0, track);
if (trackLength > 0) return trackLength;
}
function romanToArabic(input) {
const romans = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
return Array.from(input.trim().toUpperCase()).reduce((previous, current, index, array) =>
romans[array[index + 1]] > romans[current] ? previous - romans[current] : previous + romans[current], 0);
}
function notify(html, color = 'orange', length = 6) {
if (!html) return;
let div = document.body.querySelector('div.mb-notification'), animation;
if (div == null) {
div = document.createElement('div');
div.className = 'mb-notification';
div.style = `
position: sticky; margin: 0 auto; padding: 5pt; bottom: 0; left: 0; right: 0; text-align: center;
white-space: nowrap; overflow-x: clip; text-overflow: ellipsis;
font: normal 9pt "Noto Sans", sans-serif; color: white; background-color: #000b; box-shadow: 0 0 7pt 2pt #000b;
cursor: default; z-index: 10000001; opacity: 0;`;
animation = [{ opacity: 0, color: 'white', transform: 'scaleX(0.5)' }];
document.body.append(div);
} else {
animation = [{ color: 'white' }];
if ('timer' in div.dataset) clearTimeout(parseInt(div.dataset.timer));
}
div.innerHTML = html;
div.animate(animation.concat(
{ offset: 0.03, opacity: 1, color: color, transform: 'scaleX(1)' },
{ offset: 0.80, opacity: 1, color: color },
{ offset: 1.00, opacity: 0 },
), length * 1000);
div.dataset.timer = setTimeout(elem => { elem.remove() }, length * 1000, div);
}
function getEntityFromCache(cacheName, entity, id, param) {
if (!cacheName || !entity || !id) throw 'Invalid argument';
const result = eval(`
if (!${cacheName} && '${cacheName}' in sessionStorage) try {
${cacheName} = JSON.parse(sessionStorage.getItem('${cacheName}'));
} catch(e) {
sessionStorage.removeItem('${cacheName}');
console.warn(e);
}
if (!${cacheName}) ${cacheName} = { };
if (!(entity in ${cacheName})) ${cacheName}[entity] = { };
if (param) {
if (!(param in ${cacheName}[entity])) ${cacheName}[entity][param] = { };
${cacheName}[entity][param][id];
} else ${cacheName}[entity][id];
`);
if (result) return Promise.resolve(result);
}
function mbLookupByDiscID(mbTOC, allowTOCLookup = true, anyMedia = false) {
if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
return Promise.reject('mbLookupByDiscID(…): missing or invalid TOC');
const mbDiscID = mbComputeDiscID(mbTOC), params = { };
if (allowTOCLookup || !mbDiscID) params.toc = mbTOC.join('+');
if (anyMedia) params['media-format'] = 'all';
const getReleases = (offset = 0) => mbApiRequest('discid/' + (mbDiscID || '-'), Object.assign({
inc: [
'release-groups', 'artist-credits', 'labels', 'recordings',
'artist-rels', 'label-rels', 'series-rels', 'place-rels', 'work-rels', 'url-rels', 'release-rels',
].join('+'),
offset: offset,
limit: 100,
}, params)).then(function(result) {
console.log('MusicBrainz lookup by discId/TOC successfull:', mbDiscID, '/', params, 'releases:', result.releases, 'offset:', offset);
if (result.id) console.assert(result.id == mbDiscID, 'mbLookupByDiscID ids mismatch', result.id, mbDiscID);
const releases = result.releases || [ ], result2 = {
mbDiscID: mbDiscID,
mbTOC: mbTOC,
releases: releases,
attached: Boolean(result.id),
};
return result['release-count'] > result['release-offset'] + releases.length ?
getReleases(result['release-offset'] + releases.length).then(result3 =>
Object.assign(result2, { releases: result2.releases.concat(result3.releases) })) : result2;
}).then(result => result.releases.length > 0 ? result : null);
return getReleases();
}
function mbLookupById(entityType, browsingEntityType, mbid, inc = ['aliases']) {
if (!entityType || !browsingEntityType || !mbid) throw 'Invalid argument';
[entityType, browsingEntityType] = [entityType.toLowerCase(), browsingEntityType.toLowerCase()];
inc = ['url-rels'].concat(Array.isArray(inc) ? inc : [ ]).join('+');
const loadPage = (offset = 0) => mbApiRequest(entityType, {
[browsingEntityType]: mbid,
inc: inc,
offset: offset,
limit: 5000,
}).then(function(response) {
if (debugLogging && response[entityType + '-offset'] >= 5000 && response[entityType + '-offset'] % 1000 == 0)
console.info('mbLookupById offset for %s %s: %d/%d', browsingEntityType, mbid,
response[entityType + '-offset'], response[entityType + '-count']);
let results = response[({ series: 'series' }[entityType]) || entityType + 's'];
if (Array.isArray(results)) offset = response[entityType + '-offset'] + results.length; else return [ ];
results = results.filter(result => !result.video);
return offset < response[entityType + '-count'] ? loadPage(offset)
.then(Array.prototype.concat.bind(results)) : results;
});
return loadPage();
}
function mbGetReleasesAdapter(entity) {
function getReleases(entity, mbid, params) {
if (!entity || !mbid) throw 'Invalid argument';
const safeErrorHandler = reason => (console.warn(reason), null);
const workers = [mbApiRequest(entity + '/' + mbid, { inc: 'aliases+release-rels+release-group-rels+url-rels' }).then(function({relations}) {
if (!relations) throw `Assertion failed: relations missing for ${entity} ${mbid}`;
return relations.filter(relation => ['release_group', 'release'].includes(relation['target-type']))
.map(relation => Object.assign({ level: relation['target-type'], relationType: relation.type },
relation[relation['target-type']]))
.filter((target1, index, tracks) => tracks.findIndex(target2 => ['level', 'relationType', 'id']
.every(prop => target2[prop] == target1[prop])) == index);
}).catch(safeErrorHandler)];
if (params) Array.prototype.unshift.apply(workers, params.map(param =>
mbLookupById('release', param, mbid, ['aliases', 'release-groups', 'url-rels', 'release-group-level-rels'])
.then(releases => releases.map(release => Object.assign({ level: 'release', relationType: param }, release)), safeErrorHandler)));
return Promise.all(workers).then(results => (results = Array.prototype.concat.apply([ ],
results.filter(Boolean))).length > 0 ? results : null);
}
if (entity) return {
artist: mbid => getReleases(entity, mbid, ['artist', 'track_artist']),
label: mbid => getReleases(entity, mbid, [entity]),
series: mbid => getReleases(entity, mbid),
place: mbid => getReleases(entity, mbid),
}[entity]; else throw 'Invalid argument';
}
function sameTitleMapper(entry, title, cmpFn = sameStringValues, normFn = str => str.trim()) {
const compareTo = root => (root = root.title || root.name) && cmpFn(normFn(root), normFn(title));
return entry && title && (compareTo(entry) || entry.aliases && entry.aliases.some(compareTo));
}
function seedTitleNorm(title, formData) {
if (!title) return title;
const rxBonus = /\b(?:bonus(?:\s+tracks?)?|extra tracks?)\b/.source;
title = [
[new RegExp('\\s+\\(' + rxBonus + '\\)$', 'i'), ''],
[new RegExp('\\s+\\[' + rxBonus + '\\]$', 'i'), ''],
].reduce((title, subst) => title.replace(...subst), title);
const rxLive = /\b(?:live|(?:en|ao) (?:vivo|directo?))\b/.source;
// if (formData && formData.getAll('type').includes('Live') && ![
// '\\s+\\(' + rxLive + '[^\\(\\)]*\\)$',
// '\\s+\\[' + rxLive + '[^\\[\\]]*\\]$',
// '\\s+-\\s+' + rxLive + '$',
// ].some(rx => new RegExp(rx, 'i').test(title))) title += ' (live)';
return title;
}
function instrumentResolver(creditType) {
if (!creditType) return Promise.reject('Credit type is missing');
if (['Programming'].includes(creditType)) return Promise.reject('Explicitly not an instrument');
const creditedAs = ({
'Guitars': 'Guitar', 'Flutes': 'Flute', 'Horns': 'Horn',
'Keyboards': 'Keyboard', 'Synth': 'synthesizer', 'Electronics': 'Electronic Instruments',
'Drum Programming': 'Drums Programming', 'Handbells': 'Handbell',
}[creditType]) || creditType, _creditType = ({
'Lead Guitar': 'Guitar', 'Rhythm Guitar': 'Guitar', 'Cigar Box Guitar': 'Guitar', 'Fretless Guitar': 'Guitar', 'Lute Guitar': 'Guitar', 'Requinto Guitar': 'Guitar', 'Selmer-Maccaferri Guitar': 'Guitar', 'Semistrunnaya Gitara': 'Guitar', 'Twelve-String Guitar': 'Guitar',
'7-string Acoustic Guitar': 'Acoustic Guitar', '12-String Acoustic Guitar': 'Acoustic Guitar', 'Semi-Acoustic Guitar': 'Guitar',
'7-string Electric Guitar': 'Electric Guitar', '8-string Bass Guitar': 'Bass Guitar', 'Piccolo Bass Guitar': 'Bass Guitar',
'6-String Bass': 'Bass', '12-String Bass': 'Bass', 'Acoustic Piccolo Bass': 'Bass', 'Arco Bass': 'Bass', 'Brass Bass': 'Bass',
'5-String Banjo': 'Banjo', '6-String Banjo': 'Banjo', 'Cello Banjo': 'Banjo', 'Open-Back Banjo': 'Banjo', 'Piccolo Banjo': 'Banjo', 'Plectrum Banjo': 'Banjo', 'Resonator Banjo': 'Banjo',
'Baby Grand Piano': 'Grand Piano', 'Concert Grand Piano': 'Grand Piano', 'Parlour Grand Piano': 'Grand Piano', 'Player Piano': 'Piano',
'Five-String Violin': 'Violin', 'Five-String Viola': 'Viola', 'Viola Braguesa': 'Viola', 'Viola Kontra': 'Viola', 'Viola Nordestina': 'Viola', 'Viola da Terra': 'Viola', 'Viola de Cocho': 'Viola', 'Violão de sete cordas': 'Viola',
'Bolivian Flute': 'Flute', 'Free-reed Flute': 'Flute', 'Overtone Flute': 'Flute', 'Piccolo Flute': 'Flute',
'C Melody Saxophone': 'Saxophone', 'Subcontrabass Saxophone': 'Contrabass Saxophone',
'Contrabass Trombone': 'Trombone', 'Soprano Trombone': 'Trombone',
'Valve Trumpet': 'Trumpet', 'Baritone Clarinet': 'Clarinet', 'Bass Tuba': 'Tuba', 'Contra-Alto Clarinet': 'Alto Clarinet', 'Hunting Horn': 'Horn',
'Positive Organ': 'Organ', 'Steirische Harmonika': 'Harmonica', 'Electronic Drums': 'Drums',
}[creditedAs]) || creditedAs;
if (!mbInstrumentsCache) {
mbInstrumentsCache = GM_getValue('mb_instruments_cache', { });
GM_addValueChangeListener('mb_instruments_cache', (name, oldVal, newVal, remote) =>
{ if (remote) mbInstrumentsCache = newVal });
}
const resultAdapter = mbid => [{
id: mbid,
creditedAs: creditedAs != _creditType ? creditedAs.replace(...untitleCase) : undefined,
}];
const cacheKey = Object.keys(mbInstrumentsCache).find(key => key.toLowerCase() == _creditType.toLowerCase());
if (cacheKey) return Promise.resolve(resultAdapter(mbInstrumentsCache[cacheKey]));
const queryInstrument = creditType => mbApiRequest('instrument', {
query: `instrument:"${creditType}" OR alias:"${creditType}"`,
}).then(function(results) {
if (debugLogging) console.debug('Lookup results for "%s":', creditType, results.instruments);
if ((results = results.instruments).length <= 1) return results;
let filtered = results.filter(instrument => sameStringValues(instrument.name, creditType));
if (filtered.length <= 0) filtered = results
.filter(instrument => sameTitleMapper(instrument, creditType));
if (filtered.length <= 0) filtered = results
.filter(instrument => similarStringValues(instrument.name, creditType));
// if (filtered.length <= 0) filtered = results
// .filter(instrument => sameTitleMapper(instrument, creditType, similarStringValues));
// if (filtered.length <= 0 && /\w+s$/.test(creditType)) filtered = results
// .filter(instrument => sameTitleMapper(instrument, creditType.slice(0, -1)));
return filtered;
});
return queryInstrument(_creditType).then(function(instruments) {
if (instruments.length > 0) return instruments;
const allInstruments = GM_getValue('instruments');
return (allInstruments ? Promise.resolve(allInstruments) : fetchAllInstruments().then(function(allInstruments) {
if (!allInstruments || allInstruments.length <= 0) return Promise.reject('Assertion failed: no instruments found');
GM_setValue('instruments', allInstruments);
return allInstruments;
})).then(function(allInstruments) {
if (!allInstruments.includes(creditType))
return Promise.reject(`Resolved as non instrument (${creditType})`);
return (/\w+s$/.test(creditType) ? queryInstrument(creditType.slice(0, -1)).then(function(instruments) {
return instruments.length > 0 ? instruments : Promise.reject('No singular matches');
}) : Promise.reject('Not plural')).catch(reason => [{
id: '0a06dd9a-92d6-4891-a699-2b116a3d3f37', // other instruments
creditedAs: creditType.replace(...untitleCase),
}]);
});
}).then(function(instruments) {
if (instruments.length > 1) console.warn('Ambiguous instrument binding for %s:', creditType, instruments);
mbInstrumentsCache[_creditType] = instruments[0].id;
GM_setValue('mb_instruments_cache', mbInstrumentsCache);
return resultAdapter(instruments[0].id);
});
}
function guessTextRepresentation(formData, literals) {
if (!formData || typeof formData != 'object' || !literals || typeof literals != 'object') throw 'Invalid argument';
const language = detectLanguage(literals);
if (language) {
formData.set('language', language);
console.log('Guessed language from charset analysis:', language);
}
//if (!formData.has('language')) formData.set('language', 'mul');
const script = detectScript(literals);
if (script) {
formData.set('script', script);
console.log('Guessed script from charset analysis:', script);
}
//if (!formData.has('script')) formData.set('script', 'Qaaa');
}
function getMediaFingerprint(session) {
const tocEntries = getTocEntries(session), digests = getTrackDetails(session);
let fingerprint = ` Track# │ Start │ End │ CRC32 │ ARv1 │ ARv2 │ Peak
──────────────────────────────────────────────────────────────────────`;
for (let trackIndex = 0; trackIndex < tocEntries.length; ++trackIndex) {
const getTOCDetail = (key, width = 6) => tocEntries[trackIndex][key].toString().padStart(width);
const getTrackDetail = (key, callback, width = 8) => Array.isArray(digests[key])
&& digests[key].length == tocEntries.length && digests[key][trackIndex] != null ?
callback(digests[key][trackIndex]) : width > 0 ? ' '.repeat(width) : '';
const getTrackDigest = (key, width = 8) => !logScoreTest(logStatus => 'deductions' in logStatus
&& logStatus.deductions.some(RegExp.prototype.test.bind(/\b(?:CRC calculations)\b/i))) ?
getTrackDetail(key, value => value.toString(16).toUpperCase().padStart(width, '0'), 8) : '<invalid setup>';
fingerprint += '\n' + [
getTOCDetail('trackNumber'), getTOCDetail('startSector'), getTOCDetail('endSector'),
getTrackDigest('crc32'), getTrackDigest('arv1'), getTrackDigest('arv2'),
getTrackDetail('peak', value => (value[0] / 1000).toFixed(value[1])),
//getTrackDetail('preGap', value => value.toString().padStart(6)),
].map(column => ' ' + column + ' ').join('│').trimRight();
}
return fingerprint;
}
function seedFromTorrent(formData, torrent, torrentReference = GM_getValue('insert_upload_reference', false)) {
function addArtist(artist, index, artists) {
formData.set(`artist_credit.names.${++artistIndex}.name`, artist.name);
formData.set(`artist_credit.names.${artistIndex}.artist.name`, artist.name);
if (index < artists.length - 1) formData.set(`artist_credit.names.${artistIndex}.join_phrase`,
index < artists.length - 2 ? ', ' : ' & ');
}
if (!formData || typeof formData != 'object') throw 'Invalid argument';
if (torrent.group.name) formData.set('name', decodeHTML(torrent.group.name));
if (torrent.group.releaseType != 21) {
formData.set('type', ({ 5: 'EP', 9: 'Single' }[torrent.group.releaseType]) || 'Album');
switch (torrent.group.releaseType) {
case 3: formData.append('type', 'Soundtrack'); break;
case 6: case 7: formData.append('type', 'Compilation'); break;
case 11: /*case 14: */case 18: formData.append('type', 'Live'); break;
case 13: formData.append('type', 'Remix'); break;
case 15: formData.append('type', 'Interview'); break;
case 16: formData.append('type', 'Mixtape/Street'); break;
case 17: formData.append('type', 'Demo'); break;
case 19: formData.append('type', 'DJ-mix'); /*formData.append('type', 'Compilation');*/ break;
}
}
let artistIndex = -1;
if (torrent.group.releaseType == 19 && torrent.group.musicInfo
&& torrent.group.musicInfo.dj && torrent.group.musicInfo.dj.length > 0)
torrent.group.musicInfo.dj.forEach(addArtist);
else if ([7, 19].includes(torrent.group.releaseType))
formData.set('artist_credit.names.0.mbid', mb.spa.VA);
else if (torrent.group.musicInfo)
for (let role of ['dj', 'artists']) if (artistIndex < 0)
torrent.group.musicInfo[role].forEach(addArtist);
formData.set('status', [14, 18].includes(torrent.group.releaseType) ? 'bootleg' : 'official');
if (torrent.torrent.remasterYear) formData.set('events.0.date.year', torrent.torrent.remasterYear);
let [labels, catNos] = ['RecordLabel', 'CatalogueNumber'].map(prop => (value => value ? decodeHTML(value)
.split(rxEditionSplitter).map(value => value.trim()).filter(Boolean) : [ ])(torrent.torrent['remaster' + prop]));
[labels, catNos] = [
labels.map(label => rxNoLabel.test(label) ? noLabel : label),
Array.prototype.concat.apply([ ], catNos.map(catNo => rxNoCatno.test(catNo) ? [ ] : catNoMapper(catNo))),
].map(values => values.filter((s1, n, a) => a.findIndex(s2 => s2.toLowerCase() == s1.toLowerCase()) == n));
let labelIndex = 0;
for (let label of labels) for (let catNo of catNos) {
if (!label && !catNo) continue;
const prefix = `labels.${labelIndex++}`;
if (label) if (!rxNoLabel.test(label)) formData.set(`${prefix}.name`, label);
else formData.set(`${prefix}.mbid`, mb.spl.noLabel);
if (catNo) {
formData.set(`${prefix}.catalog_number`, rxNoCatno.test(catNo) ? '[none]' : catNo);
const barcode = catNo.replace(/\W+/g, '');
if (/^\d{10,13}$/.test(barcode)/* && !formData.has('barcode')*/) formData.set('barcode', barcode);
}
}
if (torrent.torrent.remasterTitle) {
const editionTitle = decodeHTML(torrent.torrent.remasterTitle).split(/\s*[\/\,\|]\s*/)
.map(t => t.replace(/^(?:CD|Re-?issue|Re-?press)$/i, '').replace(...untitleCase)).filter(Boolean);
if (editionTitle.length > 0) formData.set('comment', editionTitle.join(' / '));
}
if (torrentReference) formData.set('edit_note', ((formData.get('edit_note') || '') + '\nReference upload id: ' +
document.location.origin + '/torrents.php?torrentid=' + torrent.torrent.id).trimLeft());
}
function seedFromTOCs(formData, mbTOCs) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
for (let discIndex = 0; discIndex < mbTOCs.length; ++discIndex) {
formData.set(`mediums.${discIndex}.format`, 'CD');
formData.set(`mediums.${discIndex}.toc`, mbTOCs[discIndex].join(' '));
}
let editNote = (formData.get('edit_note') || '') + '\nSeeded from EAC/XLD ripping ' +
(mbTOCs.length > 1 ? 'logs' : 'log').trimLeft();
return getSessions(torrentId).catch(console.error).then(function(sessions) {
if (GM_getValue('mb_seed_with_fingerprints', false) && Array.isArray(sessions) && sessions.length > 0)
editNote += '\n\n' + (sessions.length > 1 ? 'Media fingerprints' : 'Medium fingerprint') + ' :\n' +
sessions.map(getMediaFingerprint).join('\n') + '\n';
formData.set('edit_note', editNote);
return formData;
});
}
function seedFromDiscogs(formData, discogsId, params, cdLengths) {
if (formData && typeof formData == 'object' && discogsId > 0) params = Object.assign({
tracklist: true, groupTracks: true, alignWithTOCs: false,
mbidLookup: true, recordingsLookup: true, lookupArtistsByRecording: true, rgLookup: true,
searchSize: GM_getValue('mbid_search_size', 30),
maxFetchDiscogsReleases: GM_getValue('max_fetch_discogs_releases', 64),
languageIdentifier: GM_getValue('external_language_id', true),
composeAnnotation: GM_getValue('compose_annotation', true),
openInconsistent: GM_getValue('open_inconsistent', true),
assignUncertain: GM_getValue('assign_uncertain', false),
createMissingEntities: GM_getValue('mb_create_entities', 1),
openCreatedEntries: GM_getValue('mb_open_new_entries', 2),
createAliases: GM_getValue('mb_create_aliases', 1),
extendedMetadata: false, rgRelations: false, releaseRelations: false,
recordingRelations: false, workRelations: false, preferTrackRelations: false,
}, params); else throw 'Invalid argument';
return dcApiRequest('releases/' + discogsId).then(function(release) {
function addLookupEntry(entity, entry, context) {
console.assert(entity && entry && context);
if (!entity || !entry || !context) throw 'Invalid argument';
console.assert(entry.id > 0 && entry.name, entry);
if (!(entry.id > 0) || !entry.name) return;
if (!(entity in lookupIndexes)) lookupIndexes[entity] = { };
if (!(entry.id in lookupIndexes[entity]))
lookupIndexes[entity][entry.id] = { name: entry.name, anv: entry.anv, contexts: [ ] };
if (!lookupIndexes[entity][entry.id].contexts.includes(context))
lookupIndexes[entity][entry.id].contexts.push(context);
}
function addCredit(entity, context, entry, modifiers) {
if (!entity || !context || !entry) throw 'Invalid argument';
if (!(entity in credits)) credits[entity] = { };
if (!(context in credits[entity])) credits[entity][context] = [ ];
if (credits[entity][context].some(_entry => _entry.id == entry.id)) return;
const _entry = { id: entry.id, name: entry.name, anv: entry.anv };
if (modifiers && modifiers.length > 0) _entry.modifiers = modifiers;
credits[entity][context].push(_entry);
}
function addCredits(roles, artist) {
const modifiers = ['Soloist', 'Guest'].filter(role => roles.includes(role));
const realRoles = roles.filter(role => !modifiers.includes(role));
if (modifiers.length > 0 && realRoles.length > 0) for (let role of realRoles)
addCredit('artist', role, artist, modifiers);
else for (let role of roles) addCredit('artist', role, artist);
}
function findRelationLevels(entity, type) {
if (!(entity in relationsIndex)) return [ ];
let levels = Object.keys(relationsIndex[entity]).filter(level => type in relationsIndex[entity][level]);
return levels;
}
function resolveExtraArtists(roots, roleTrackEvaluator = role => !findRelationLevels('artist', role).some(isReleaseLevel)) {
const extraArtists = { };
console.assert(Array.isArray(roots) && roots.length > 0, roots);
console.assert(typeof roleTrackEvaluator == 'function', roleTrackEvaluator);
roots.forEach(function(root, index) {
if (root.extraartists) root.extraartists.forEach(function(extraArtist) {
let roles = getRoles(extraArtist);
if (root == release) if (extraArtist.tracks) {
const tracks = extraArtist.tracks.split(',').map(track => track.trim());
if (!roots.some(position => (position = position.position) && tracks.some(function(track) {
let range = /^(\S+)(?:\s*(?:to|[–])\s*|\s+(?:-)\s+)(\S+)$/i.exec(track);
if (range == null) return trackPosMapper(position) == trackPosMapper(track);
range = [position].concat(range.slice(1)).map(trackPosMapper);
return range[0].localeCompare(range[1]) >= 0 && range[0].localeCompare(range[2]) <= 0;
}))) return;
} else if (!params.preferTrackRelations) roles = roles.filter(roleTrackEvaluator);
if (roles.length <= 0) return;
if (!(extraArtist.id in extraArtists)) extraArtists[extraArtist.id] = {
id: extraArtist.id,
name: extraArtist.name,
anv: extraArtist.anv,
roles: [ ],
};
for (let role of roles) if (!extraArtists[extraArtist.id].roles.includes(role))
extraArtists[extraArtist.id].roles.push(role);
});
});
if (Object.keys(extraArtists).length > 0) return Object.values(extraArtists);
}
function resolveArtists(...roots) {
for (let index = roots.length; index > 0; --index)
if (roots[index - 1].parents) roots.splice(index - 1, 0, ...roots[index - 1].parents);
const artists = roots.map(root => root.artists).reverse()
.find(artists => Array.isArray(artists) && artists.length > 0);
if (!artists) return null; else for (let index = artists.length; index > 0; --index)
if (index > 1 && artists[index - 2].id == artists[index - 1].id)
artists[index - 2].join = artists[index - 1].join;
return {
artists: artists.filter((item1, index, array) => array.findIndex(item2 => item2.id == item1.id) == index),
extraartists: resolveExtraArtists(roots),
};
}
const sameArtists = (...artists) => artists.length > 0 && artists.every(artist1 => artists.every(function(artist2) {
if (artist2.id != artist1.id) return false; else if (!artist2.roles && !artist1.roles) return true;
const roles = [artist1, artist2].map(getRoles);
return roles[0].length == roles[1].length && roles[0].every(role => roles[1].includes(role));
}));
function sameTrackArtists(...roots) {
if (roots.length < 2 && !roots.includes(release)) roots.unshift(release);
if (!roots.some(Boolean)) return true; else if (!roots.every(Boolean)) return false;
const props = ['artists', 'extraartists'];
return props.every(prop => !roots.some(root1 => root1[prop] && !root1[prop].every(artist1 =>
roots.every(root2 => root2[prop] && root2[prop].some(artist2 => sameArtists(artist1, artist2))))));
}
function trackMainArtists(track) {
let artists = track.trackArtists;
if (artists && (artists = Array.prototype.concat.apply([ ], artists.map(artists => artists.artists))).length > 0)
return artists.filter((artist1, index, artists) =>
artists.findIndex(artist2 => sameArtists(artist2, artist1)) == index);
return (artists = resolveArtists(release, track)) != null ? artists.artists : null;
}
function seedArtists(root, prefix, offset = 0) {
function seedArtist(artist, index, array, prefix, offset = 0) {
prefix = `${prefix || ''}artist_credit.names.${offset + index}`;
formData.set(`${prefix}.artist.name`, capitalizeName(stripDiscogsNameVersion(artist.name)));
if (!noCreditAsArtists.includes(artist.id))
formData.set(`${prefix}.name`, capitalizeName(creditedName(artist)));
if (artist.join) formData.set(`${prefix}.join_phrase`, fmtJoinPhrase(artist.join));
else if (index < array.length - 1)
formData.set(`${prefix}.join_phrase`, index < array.length - 2 ? ', ' : ' & ');
//else formData.delete(`${prefix}.join_phrase`);
addLookupEntry('artist', artist, prefix);
}
function processExtraArtists(extraArtists, join) {
if (!extraArtists || extraArtists.length <= 0 || !join) return;
if (offset > 0) formData.set(`${prefix || ''}artist_credit.names.${offset - 1}.join_phrase`,
fmtJoinPhrase(join));
extraArtists.forEach((extraArtist, index, array) =>
{ seedArtist(extraArtist, index, array, prefix, offset) });
offset += extraArtists.length;
}
if (!root) throw 'Invalid argument';
if (!Array.isArray(root.artists) || root.artists.length <= 0) return offset;
root.artists.forEach((artist, index, array) => { seedArtist(artist, index, array, prefix, offset) });
offset += root.artists.length;
if (!root.extraartists) return offset;
const byRole = role => root.extraartists.filter(extraArtist => getRoles(extraArtist).some(_role =>
(_role = rxRoleParser.exec(_role)) != null && _role[1].toLowerCase() == role.toLowerCase()))
.filter((artist1, index, array) => array.findIndex(artist2 => artist2.id == artist1.id) == index);
let extraArtists = byRole('Featuring').filter(extraArtist =>
!root.artists.some(artist => artist.id == extraArtist.id));
processExtraArtists(extraArtists, 'feat.');
//processExtraArtists(extraArtists = byRole('Remix'), 'remixed by');
return offset;
}
function addUrlRef(url, linkType) {
formData.set(`urls.${++urlRelIndex}.url`, url);
if (linkType != undefined) formData.set(`urls.${urlRelIndex}.link_type`, linkType);
}
function searchQueryBuilder(entity, discogsId) {
function addField(field, expr) {
if (!(field in query)) query[field] = [ ];
query[field].push(expr);
}
if (!(entity in lookupIndexes) || !(discogsId in lookupIndexes[entity])) return;
const addName = name => { for (let field of [entity, 'alias', 'sortname']) addField(field, '"' + name + '"') };
const query = { }, name = stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name);
addName(name);
switch (entity) {
case 'label': case 'place': {
const bareName = labelMapper(name.replace(...rxBareLabel));
for (let field of [entity, 'alias']) addField(field, '"' + bareName + '"');
break;
}
}
if (lookupIndexes[entity][discogsId].anv && lookupIndexes[entity][discogsId].anv != name)
addName(lookupIndexes[entity][discogsId].anv);
return Object.keys(query).map(field => query[field].map(expr => `${field}:(${expr})`).join(' OR ')).join(' OR ');
}
function calcTime(tracks) {
const duration = tracks.reduce(function(length, track) {
const duration = /\b(\d+):(\d+)\b/.exec(track.duration);
return duration != null ? length + parseInt(duration[1]) * 60 + parseInt(duration[2]) : undefined;
}, 0);
return duration > 0 ? Math.floor(duration / 60).toString() + ':' +
(duration % 60).toString().padStart(2, '0') : undefined;
}
function layoutMatch(media) {
if (!media) return -Infinity; else if (!Array.isArray(cdLengths) || cdLengths.length <= 0) return 0;
if ((media = media.filter(isCD)).length != cdLengths.length) return -2;
if (media.every((medium, mediumIndex) => medium.tracks.length == cdLengths[mediumIndex])) return 3;
if (media.every((medium, mediumIndex) => medium.tracks.length == cdLengths[mediumIndex]
|| medium.format == 'Enhanced CD' && medium.tracks.length > cdLengths[mediumIndex])) return 2;
if (cdLengths.length > 1) {
const index = { };
for (let key of cdLengths) if (!(key in index))
index[key] = media.filter(medium => medium.tracks.length == key).length;
if (Object.keys(index).every(key1 => index[key1] == cdLengths.filter(key2 =>
key2 == parseInt(key1)).length)) {
notify('Tracks layout matched to reordered logs', 'orangered');
return 1;
}
}
return -1;
}
function parseTracklist(trackParser = rxParsingMethods[0], collapseSubtracks = false) {
if (!(trackParser instanceof RegExp)) throw 'Invalid argument';
if (!release.tracklist) return null;
const media = [ ], romanNumbers = release.tracklist.every(track => romanToArabic(track.position) > 0);
let lastMediumId, heading;
(function addTracks(tracklist, parents) {
if (Array.isArray(tracklist)) tracklist.forEach(function(track) {
if (track.type_ == 'index' && !collapseSubtracks) return addTracks(track.sub_tracks, (parents || [ ])
.concat({ title: track.title, artists: track.artists, extraartists: track.extraartists }));
else if (track.type_ == 'heading') return heading = track.title != '-' && [
[/:+$/, ''],
[/^(?:Bonus(?:\s+Tracks?)?)(?:(?:\s*-|:)\s+)?/, ''],
].reduce((heading, subst) => heading.replace(...subst), track.title) || undefined;
const parsedTrack = trackParser.exec(track.position.trim());
let [mediumFormat, mediumId, number] = parsedTrack != null ?
parsedTrack.slice(1) : [undefined, undefined, track.position.trim()];
if (!mediumFormat && !romanNumbers && /^[A-Z]\d*$/.test(number)) mediumFormat = 'LP';
mediumId = (mediumFormat || '') + (mediumId || '');
if (lastMediumId == undefined || !parents && mediumId !== lastMediumId) {
let title;
for (let subst of [
[/^(?:B(?:R?D|R))$/, 'Blu-ray'],
[/^(?:LP)$/, 'Vinyl'],
[/^(FLAC|MP[234]|AAC|M4[AB]|OGG|Vorbis|Opus)$/i, 'Digital Media'],
]) if (subst[0].test(mediumFormat)) {
mediumFormat = subst[1];
if (RegExp.$1) title = RegExp.$1;
}
if (mediumFormat == 'CD') mediumFormat = defaultFormat;
media.push({ format: mediumFormat || defaultFormat, name: title, tracks: [ ] });
lastMediumId = mediumId;
}
let name = track.title, duration = track.duration, trackArtists;
if (track.type_ == 'index' && track.sub_tracks && track.sub_tracks.length > 0) {
name += `: ${track.sub_tracks.map(track => track.title.trim()).filter(Boolean).join(' / ')}`;
trackArtists = (trackArtists = track.sub_tracks.map(subTrack =>
!sameTrackArtists(resolveArtists(release, track, subTrack)) ?
resolveArtists(track, subTrack) : null)
.filter(Boolean)).length > 0 ? trackArtists.filter((artists1, index, array) =>
array.findIndex(artists2 => sameTrackArtists(artists2, artists1)) == index) : undefined;
if (!duration) duration = calcTime(track.sub_tracks);
}
media[media.length - 1].tracks.push({
number: number, position: track.position, heading: heading, name: name, length: duration,
artists: track.artists, extraartists: track.extraartists, trackArtists: trackArtists,
parents: parents,
});
});
})(release.tracklist);
return media;
}
function groupTracks(media, rxExtractor) {
if (layoutMatch(media = media.map(function tracksConsolidation(medium, index) {
const tracks = { };
for (let track of medium.tracks) {
if (rxExtractor instanceof RegExp) {
var trackNum = rxExtractor.exec(track.number);
console.assert(trackNum != null, track.number);
trackNum = trackNum != null ? trackNum[0] : track.number;
} else trackNum = isCD(medium) ? index + 1 : track.number;
if (!(trackNum in tracks)) tracks[trackNum] = [ ];
tracks[trackNum].push(track);
}
medium.tracks = Object.keys(tracks).sort(function(...positions) {
positions = positions.map(trackPosMapper);
return positions[0].localeCompare(positions[1]);
}).map(function(trackNo) {
const trackArtists = tracks[trackNo].map(track => !sameTrackArtists(resolveArtists(release, track)) ?
resolveArtists(track) : null).filter(Boolean);
// if (debugLogging && tracks[trackNo].length > 1) alert(`Medium tracks merged (${trackNo}/${trackArtists.length})`);
return {
number: trackNo,
name: tracks[trackNo].map(track => track.name.trim()).filter(Boolean).join(' / '),
heading: tracks[trackNo][0].heading, parents: tracks[trackNo][0].parents,
trackArtists: trackArtists.length > 0 ? trackArtists.filter((artists1, index, array) =>
array.findIndex(artists2 => sameTrackArtists(artists2, artists1)) == index) : undefined,
length: calcTime(tracks[trackNo]),
};
});
return medium;
})) > 0) return media;
}
function processFormats(mappers, applyFn) {
if (!mappers || typeof mappers != 'object') throw 'Invalid argument';
const regExp = key => new RegExp('^(?:' + key + ')$', 'i');
if (typeof applyFn == 'function') for (let key in mappers) if (mappers[key] != undefined
&& descriptors.some(RegExp.prototype.test.bind(regExp(key)))) applyFn(mappers[key]);
descriptors = descriptors.filter(descriptor =>
!Object.keys(mappers).some(key => regExp(key).test(descriptor)));
}
function openInconsistent(entity, discogsId, mbids, subpage) {
Array.from(mbids).reverse().forEach(mbid =>
{ GM_openInTab([mbOrigin, entity, mbid, subpage].filter(Boolean).join('/'), true) });
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/') + '#discography_wrapper', true);
}
function saveToCache(entity, discogsId, mbid) {
discogsBindingsCache[entity][discogsId] = mbid;
GM_setValue('discogs_to_mb_bindings', discogsBindingsCache);
}
function getCachedMBID(entity, discogsId, mbEntity = entity) {
console.assert(entity && discogsId > 0);
if (!entity || !(discogsId > 0)) throw 'Invalid argument';
if (!discogsBindingsCache) {
if (!(discogsBindingsCache = GM_getValue('discogs_to_mb_bindings'))) discogsBindingsCache = { };
else console.info('Discogs to MB bindings cache loaded:', Object.keys(discogsBindingsCache)
.map(key => `${Object.keys(discogsBindingsCache[key]).length} ${(key + 's').replace(/s(?=s$)/, '')}`).join(', '));
GM_addValueChangeListener('discogs_to_mb_bindings',
(name, oldVal, newVal, remote) => { if (remote) discogsBindingsCache = newVal });
const defaults = {
artist: {
194: mb.spa.VA, 355: mb.spa.unknown, 118760: mb.spa.noArtist, 598667: mb.spa.traditional,
967691: mb.spa.anonymous, 3538550: mb.spa.dialogue,
5942241: mb.spa.noArtist, // nature sounds => [no artist]
},
label: {
750: '49b58bdb-3d74-40c6-956a-4c4b46115c9c', // Virgin
895: '1ca5ed29-e00b-4ea5-b817-0bcca0e04946', // RCA
1818: mb.spl.noLabel,
1866: '011d1192-6f65-45bd-85c4-0400dd45693e', // Columbia
2345: '3730c0ea-3dc2-45c3-ac5c-9d482921ea51', // Warner
5320: 'f18f3b31-8263-4de3-966a-fda317492d3d', // Decca
26126: 'c029628b-6633-439e-bcee-ed02e8a338f7', // EMI
108701: '7c439400-a83c-48bc-9042-2041711c9599', // Virgin JP
1687281: mb.spl.unknown,
},
series: {
77074: '713c4a95-6616-442b-9cf6-14e1ddfd5946', // Blue Note Records => Blue Note
},
place: {
654: '7cc76d09-fc09-4faf-8406-f9ba9d046b73', // Capitol Records
},
};
for (let entity in defaults) {
if (!(entity in discogsBindingsCache)) discogsBindingsCache[entity] = { };
for (let discogsId in defaults[entity]) if (!(discogsId in discogsBindingsCache[entity]))
discogsBindingsCache[entity][discogsId] = defaults[entity][discogsId] || null;
}
for (let entity of Object.getOwnPropertyNames(dynamicIdResolvers)) {
if (!(entity in discogsBindingsCache)) discogsBindingsCache[entity] = { };
for (let discogsId of Object.getOwnPropertyNames(dynamicIdResolvers[entity]))
if (parseInt(discogsId) > 0 && !(discogsId in discogsBindingsCache[entity]))
discogsBindingsCache[entity][discogsId] = null;
}
}
if (!(entity in discogsBindingsCache) || !(discogsId in discogsBindingsCache[entity]))
return Promise.reject('Not cached');
const verifyMBID = (mbid, updateChangedCache = true) => (rxMBID.test(mbid) ? globalXHR(`${mbOrigin}/${entity}/${mbid}`, {
method: 'HEAD', redirect: 'follow', anonymous: true,
}).then(function(response) {
if (response.status < 200 || response.status >= 400) return Promise.reject(response.statusText);
response = mbIdExtractor(response.finalUrl, mbEntity);
if (!response) return Promise.reject('Cached check failed');
console.log('Entity binding for', entity, discogsId, 'got from cache');
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} got from cache`, 'sandybrown') });
if (response != mbid) {
console.info('MB entry for %s %d has moved: %s => %s', entity, discogsId, mbid, response);
if (updateChangedCache) saveToCache(entity, discogsId, response);
}
return response;
}) : Promise.reject('Invalid format')).catch(function(reason) {
console.warn('Failed to verify %s MBID %s (%s)', entity, mbid, reason);
return mbid;
});
let resolved = discogsBindingsCache[entity][discogsId];
if (resolved != null && rxMBID.test(resolved)) return verifyMBID(resolved, true);
if (entity in dynamicIdResolvers && discogsId in dynamicIdResolvers[entity]) {
const resolved = dynamicIdResolvers[entity][discogsId];
return resolved ? !Array.isArray(resolved) ? verifyMBID(resolved, false)
: Promise.all(resolved.map(mbid => verifyMBID(mbid, false))) : Promise.resolve(resolved);
} else return Promise.resolve(null);
}
function findMBIDByCommonTitles(entity, discogsId, mbids) {
if (!entity || !(discogsId > 0) || !Array.isArray(mbids)) throw 'Invalid argument';
if (mbids.length <= 0) return Promise.reject('No MusicBrainz entries');
const getDiscogsReleases = (page = 1) => dcApiRequest([discogsEntity(entity) + 's', discogsId, 'releases'].join('/'), {
page: page,
per_page: 500,
}).then(function(response) {
if (debugLogging && response.pagination.page > 1 && response.pagination.page % 50 == 0)
console.info('getDiscogsReleases %s/%d page %d/%d', discogsEntity(entity), discogsId, response.pagination.page, response.pagination.pages);
return !(response.pagination.pages > response.pagination.page) ? response.releases
: getDiscogsReleases(response.pagination.page + 1).then(Array.prototype.concat.bind(response.releases));
});
const dcReleasesWorker = getDiscogsReleases(), recordingRoles = ['Main', 'TrackAppearance'];
const lookupMethods = [{ worker: mbGetReleasesAdapter(entity), resolver: function(dcReleases, results) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'), true);
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/'), true);
}
if (dcReleases.length <= 0) return Promise.reject('No matches by common releases');
const mutualScores = results.map(results => results ? results.reduce(function(score, result) {
const relatedReleases = [ ];
switch (result.level) {
case 'release':
for (let discogsId of getDiscogsRels(result, 'release'))
Array.prototype.push.apply(relatedReleases, dcReleases.filter(dcRelease =>
(dcRelease.type == 'release' || !dcRelease.type) && dcRelease.id == discogsId));
for (let discogsId of getDiscogsRels(result['release-group'], 'master'))
Array.prototype.push.apply(relatedReleases, dcReleases.filter(dcRelease =>
dcRelease.type == 'master' && dcRelease.id == discogsId));
break;
case 'release_group':
for (let discogsId of getDiscogsRels(result, 'master'))
Array.prototype.push.apply(relatedReleases, dcReleases.filter(dcRelease =>
dcRelease.type == 'master' && dcRelease.id == discogsId));
break;
default: console.warn('Unexpected result level:', result);
}
if (relatedReleases.length > 0) {
console.assert(relatedReleases.length < 2, relatedReleases);
const q = (!result.relationType || result.relationType == entity)
&& relatedReleases.some(dcRelease => !dcRelease.role || dcRelease.role == 'Main') ? 1 : 2/3;
if (debugLogging) console.debug('Found matching releases by existing relation:', result, relatedReleases, 'Score:', q);
return score + q;
} else return score + Math.max(...dcReleases.map(function(dcRelease) {
function titleSimilarity(root) {
if (root) if (sameTitleMapper(root, dcRelease.title, sameStringValues))
return root.title.length;
else if (sameTitleMapper(root, dcRelease.title, sameStringValues, releaseTitleNorm))
return releaseTitleNorm(root.title).length;
return 0;
}
if (entity == 'artist' && result.relationType) switch (result.relationType) {
case 'artist': if (dcRelease.trackinfo) return 0; else break;
case 'track_artist': if (!dcRelease.trackinfo) return 0; else break;
default: if (!dcRelease.role || dcRelease.role == 'Main') return 0;
}
const releaseGroup = result.level == 'release_group' ? result : result['release-group'];
const q = [0, 0];
const releaseYear = result.level == 'release' ? getReleaseYear(result.date) : NaN;
const rgYear = releaseGroup && getReleaseYear(releaseGroup['first-release-date']) || NaN;
if (!(dcRelease.year > 0)) return 0; else if (dcRelease.type == 'release' || !dcRelease.type) {
if (dcRelease.year == releaseYear) q[0] = 1; else if (dcRelease.year >= rgYear) q[0] = 1/2;
} else if (dcRelease.type == 'master') {
if (dcRelease.year == rgYear) q[0] = 1; else if (dcRelease.year <= releaseYear) q[0] = 1/2;
}
if (!(q[0] > 0)) return 0;
if ((dcRelease.type == 'release' || !dcRelease.type) && result.level == 'release')
q[1] = titleSimilarity(result);
else if (dcRelease.type == 'master') q[1] = titleSimilarity(releaseGroup);
if (!(q[1] > 0)) return 0;
let score = q[0] * ((base, confidencyLen, exp = 1, factor = 1) =>
base + Math.pow(Math.min(q[1], confidencyLen) / confidencyLen, exp) * factor * (1 - base))
(0, 5, 0.75, 0.80);
if (entity == 'artist' && (result.relationType == 'track_artist' || dcRelease.trackinfo))
score *= 2/3;
if (debugLogging) console.debug('Found matching releases:', result, dcRelease, 'Score:', score);
return score;
}));
}, 0) : 0), hiScore = Math.max(...mutualScores);
if (debugLogging) console.debug('Common titles lookup method #1: Entity:', entity,
'Discogs ID:', discogsId, 'MBIDs:', mbids, 'Scores:', mutualScores, 'HiScore:', hiScore);
if (!(hiScore > 0)) return Promise.reject('No matches by common releases');
const hiIndex = mutualScores.indexOf(hiScore);
console.assert(hiIndex >= 0, hiScore, mutualScores);
if (debugLogging && hiIndex < 0) alert('HiIndex not found! (see the log)');
const dataSize = Math.min(dcReleases.length, results[hiIndex].length);
if (!(Math.pow(hiScore, 3) * 10 >= Math.min(dataSize, 10) && hiScore * 50 >= dataSize))
if (params.assignUncertain) openUncertain();
else return Promise.reject('Matched by common releases with too low match rate');
else if (hiScore < 1) openUncertain();
console.log('Entity binding found by having score %f (%d):\n%s\n%s', hiScore, dataSize,
[dcOrigin, discogsEntity(entity), discogsId].join('/') + '#' + discogsEntity(entity),
[mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'));
if (mutualScores.filter(score => score > 0).length > 1) {
console.log('Matches by more entities:', mutualScores.map((score, index) =>
score > 0 && [mbOrigin, entity, mbids[index], 'releases'].join('/') + ' (' + score + ')').filter(Boolean));
if (params.openInconsistent) openInconsistent(entity, discogsId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'releases');
beep.play();
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (releases)');
}
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${dataSize} release(s)`, 'gold') });
return mbids[hiIndex];
} }];
if (entity == 'artist') lookupMethods.push({ worker: getArtistTracks, resolver: function(dcReleases, tracks) {
function scanReleases(dcReleases, scoreEvaluator, sizeEvaluator) {
if (!dcReleases || ![scoreEvaluator, sizeEvaluator].every(arg => typeof arg == 'function'))
throw 'Invalid argument';
dcReleases = dcReleases.filter(Boolean);
return Promise.all(tracks.map((tracks, index) => tracks && dcReleases.length > 0 ?
Promise.all(tracks.map(track => scoreEvaluator(dcReleases, track, mbids[index]))).then(scores =>
scores.reduce((total, score) => total + (score > 0 ? score : 0), 0)) : 0)).then(function(mutualScores) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'recordings'].join('/'), true);
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/'), true);
}
const hiScore = Math.max(...mutualScores);
if (debugLogging) console.debug('Common titles lookup method #2: Entity:', entity,
'Discogs ID:', discogsId, 'MBIDs:', mbids, 'Scores:', mutualScores, 'HiScore:', hiScore);
if (!(hiScore > 0)) return Promise.reject('No matches by common tracks');
const hiIndex = mutualScores.indexOf(hiScore);
console.assert(hiIndex >= 0, hiScore, mutualScores);
if (debugLogging && hiIndex < 0) alert('HiIndex not found! (see the log)');
return Promise.all([sizeEvaluator(dcReleases), uniqueIdCount(tracks[hiIndex])]).then(function(sizes) {
const dataSize = Math.min(...sizes);
if (!(Math.pow(hiScore, 3) * 10 >= Math.min(dataSize, 10) && hiScore * 50 >= dataSize))
if (params.assignUncertain) openUncertain();
else return Promise.reject('Matched by common tracks with too low score');
else if (hiScore < 1) openUncertain();
console.log('Entity binding found by having score %f (%d):\n%s\n%s', hiScore, dataSize,
[dcOrigin, discogsEntity(entity), discogsId].join('/') + '#' + discogsEntity(entity),
[mbOrigin, entity, mbids[hiIndex], 'recordings'].join('/'));
if (mutualScores.filter(score => score > 0).length > 1) {
console.log('Matches by more entities:', mutualScores.map((score, index) =>
score > 0 && [mbOrigin, entity, mbids[index], 'recordings'].join('/') + ' (' + score + ')').filter(Boolean));
beep.play();
if (params.openInconsistent) openInconsistent(entity, discogsId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'recordings');
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (tracks)');
}
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${dataSize} track(s)`, 'gold') });
return mbids[hiIndex];
});
});
}
if (dcReleases.length <= 0) return Promise.reject('No matches by common tracks');
const uniqueIdFilter = (entity1, index, entities) =>
entities.findIndex(entity2 => entity2.id == entity1.id) == index;
const uniqueIdCount = array => array.filter(uniqueIdFilter).length;
return scanReleases(dcReleases.filter(dcRelease => dcRelease.trackinfo), (dcReleases, track) => Math.max(...dcReleases.map(function(dcRelease) {
const isRecordingArtist = !dcRelease.role || recordingRoles.includes(dcRelease.role);
if ((track.relationType == 'track_artist') != isRecordingArtist
|| !sameTitleMapper(track, dcRelease.trackinfo, sameStringValues, trackTitleNorm)) return 0;
const base = 0, q = Math.pow(Math.min(trackTitleNorm(track.title).length, 5) / 5, 0.75) * 0.65;
let score = base + q * (1 - base);
if (dcRelease.year > 0 && track['first-release-date']
&& dcRelease.year == getReleaseYear(track['first-release-date'])) score *= 1.25;
if (debugLogging) console.debug('Found matching tracks (from trackinfo):', track, dcRelease, 'Score:', score);
return score;
})), uniqueIdCount).catch(function(reason) {
if (!(params.maxFetchDiscogsReleases > 0) || dcReleases.length > params.maxFetchDiscogsReleases) {
let asRelatedArtist = tracks.filter(track => track.relationType != 'track_artist');
asRelatedArtist = uniqueIdCount(asRelatedArtist) * 2 > uniqueIdCount(tracks);
dcReleases = dcReleases.filter(dcRelease => asRelatedArtist ?
dcRelease.role && ['Appearance', 'TrackAppearance'].includes(dcRelease.role)
: !dcRelease.role || ['Main', 'TrackAppearance'].includes(dcRelease.role));
}
if (dcReleases.length <= 0 || params.maxFetchDiscogsReleases > 0
&& dcReleases.length > params.maxFetchDiscogsReleases) return Promise.reject(reason);
const processTracklists = (dcReleases, callBack) => Promise.all(dcReleases.map(dcRelease =>
dcApiRequest((dcRelease.type || 'release') + 's/' + dcRelease.id).then(callBack).catch(reason => (console.warn(reason), 0))));
const scoreEvaluator = (dcReleases, track, mbid) => processTracklists(dcReleases, function(dcRelease) {
console.assert(dcRelease.tracklist, dcRelease);
const prop = track.relationType == 'track_artist' ? 'artists' : 'extraartists';
const isRootArtist = (...roots) => roots.some(root => root && (root = root[prop])
&& root.some(artist => artist.id == parseInt(discogsId)));
let score = Math.max(...dcRelease.tracklist.map(function(dcTrack) {
function trackScore(dcTrack) {
function computeScore(base, confidencyLen, exp, factor) {
let score = Math.min(trackTitleNorm(track.title).length, confidencyLen) / confidencyLen;
score = base + Math.pow(score, exp) * factor * (1 - base);
if (!sameTitleMapper(track, dcTrack.title, sameStringValues, trackTitleNorm)) score *= 0.8;
if (dcRelease.year > 0 && track['first-release-date']
&& dcRelease.year == getReleaseYear(track['first-release-date'])) score *= 1.25;
return score;
}
let lengthDelta = getTrackLength(dcTrack), score;
lengthDelta = track.length > 0 && lengthDelta > 0 ?
Math.abs(lengthDelta - track.length) : undefined;
if (lengthDelta > 5000 || !sameTitleMapper(track, dcTrack.title,
lengthDelta < 1000 ? similarStringValues : sameStringValues, trackTitleNorm)) return 0;
if (isNaN(lengthDelta)) {
score = computeScore(0, 5, 0.75, 0.65);
if (debugLogging) console.debug('Found matching tracks (times not compared):', track, dcTrack, 'Score:', score);
} else {
score = computeScore(0, 5, 0.75, 0.80) * (1 - Math.pow(lengthDelta / 5000, 1) / 2);
if (debugLogging) console.debug('Found matching tracks (times compared):', track, dcTrack, lengthDelta, 'Score:', score);
}
return score;
}
switch (dcTrack.type_) {
case 'track': return isRootArtist(dcTrack, dcRelease) ? trackScore(dcTrack) : 0;
case 'index': return dcTrack.sub_tracks ? Math.max(...dcTrack.sub_tracks.map(subTrack =>
isRootArtist(subTrack, dcTrack, dcRelease) ? trackScore(subTrack) : 0)) : 0;
default: return 0;
}
}));
if (!(score > 0)) return 0; else if (!mbIdExtractor(mbid) || track.level != 'recording'
|| !dcRelease.artists || !dcRelease.artists.some(artist => artist.id == parseInt(discogsId)))
return score;
return mbLookupById('release', 'recording', track.id, ['artist-credits']).then(function(releases) {
console.assert(releases.length > 0);
if (releases.length > 1) score *= 1 + (releases.length - 1) / 3;
if (releases.some(release => release?.['artist-credit']
.some(({artist}) => artist && artist?.id.toLowerCase() == mbid))) score *= 2;
return score;
}).catch(reason => (console.warn(reason), score));
}).then(scores => (scores = Math.max(...scores)) > 0 ? scores : 0);
const sizeEvaluator = dcReleases => processTracklists(dcReleases.filter(uniqueIdFilter), dcRelease =>
dcRelease.tracklist.reduce((n, track) => n + (track.type_ == 'track' ? 1
: track.type_ == 'index' ? track.sub_tracks ? track.sub_tracks.length : 1 : 0), 0))
.then(counts => counts.reduce((total, count) => total + count, 0));
return scanReleases(dcReleases, scoreEvaluator, sizeEvaluator);
});
} });
const discogsEntry = dcApiRequest(discogsEntity(entity) + 's/' + discogsId).catch(reason => null);
return Promise.all(mbids.map(mbid => entity in discogsBindingsCache
&& Object.values(discogsBindingsCache[entity]).includes(mbid) ? Promise.resolve(null) : Promise.all([
mbApiRequest(entity + '/' + mbid, { inc: `aliases+url-rels+${entity}-rels` }),
discogsEntry,
]).then(function([entry, discogsEntry]) {
function hasSameRelatives(prop, relationType, forward) {
if (!prop || !relationType || !discogsEntry[prop]) return false;
const mbRelatives = entry.relations.filter(relation => relation['target-type'] == entity
&& relation.type == relationType
&& (forward === undefined || relation.direction == (forward ? 'forward' : 'backward')))
.map(relation => relation[entity]);
return mbRelatives.length > 0 && discogsEntry[prop].some(discogsRelative =>
mbRelatives.some(mbRelative => entity in discogsBindingsCache
&& discogsBindingsCache[entity][discogsRelative.id] == mbRelative.id
|| matchNameVariant(discogsRelative, mbRelative.name) || mbRelative.aliases
&& mbRelative.aliases.some(alias => matchNameVariant(discogsRelative, mbRelative.name))));
}
const discogsIds = getDiscogsRels(entry, discogsEntity(entity));
if (discogsIds.includes(parseInt(discogsId))) return discogsIds.length < 2 ? entry.id : true;
if (discogsIds.length > 0) return null;
if (discogsEntry.urls && discogsEntry.urls.some(function(url) {
try { url = new URL(url) } catch(e) { return false }
const staticPath = url => (url.hostname + url.pathname + url.search).toLowerCase();
return entry.relations.filter(relation => relation['target-type'] == 'url').some(function(relation) {
try { relation = new URL(relation.url.resource) } catch(e) { return false }
return staticPath(relation) == staticPath(url);
});
})) {
if (debugLogging) console.debug('Same %ss found by having same url(s):', entity, discogsEntry, entry);
return entry.id;
} else if (entity == 'artist' && (hasSameRelatives('members', 'member of band', false)
|| hasSameRelatives('groups', 'member of band', true) || hasSameRelatives('aliases', 'is person'))
|| entity == 'label' && hasSameRelatives('sublabels', 'label ownership', true)) {
if (debugLogging) console.debug('Same %ss found by having same relative(s):', entity, discogsEntry, entry);
return entry.id;
} else if (entity == 'artist' && discogsEntry.realname
&& !sameStringValues(discogsEntry.realname, stripDiscogsNameVersion(discogsEntry.name))
&& entry.aliases && entry.aliases.some(alias => alias.type == 'Legal name'
&& !sameStringValues(alias.name, entry.name) && sameStringValues(alias.name, discogsEntry.realname))) {
if (debugLogging) console.debug('Same %ss found by having same legal name:', entity, discogsEntry, entry);
return entry.id;
} else if (entry.disambiguation && discogsEntry.profile) {
const length = Math.min(entry.disambiguation.length, discogsEntry.profile.length);
if (length >= 10 && jaroWinklerSimilarity(entry.disambiguation.toLowerCase(),
discogsEntry.profile.toLowerCase()) >= 0.98 - (length - 10) / 2000) {
if (debugLogging) console.debug('Same %ss found by having same profile info:',
entity, discogsEntry, entry);
return entry.id;
}
}
return true;
}).catch(function(reason) {
console.warn(reason);
return true;
}))).then(function(statuses) {
let lookupMethod = statuses.filter(mbIdExtractor);
if (lookupMethod.length == 1) {
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} found by sharing same relation`, 'cyan') });
return lookupMethod[0];
} else lookupMethod = (methodIndex = 0) => methodIndex < lookupMethods.length ?
(['worker', 'resolver'].every(fn => typeof lookupMethods[methodIndex][fn] == 'function') ?
Promise.all([dcReleasesWorker].concat(statuses.map((status, index) => status
&& lookupMethods[methodIndex].worker(mbids[index]).catch(console.warn))))
.then(resolved => lookupMethods[methodIndex].resolver(resolved.shift(), resolved))
: Promise.reject('Method not implemented'))
.catch(reason => lookupMethod(methodIndex + 1)) : Promise.reject('Not found by common titles');
return lookupMethod();
}).then(function(mbid) {
saveToCache(entity, discogsId, mbid);
return mbid;
});
}
function findMBID(entity, discogsId) {
let promise = getCachedMBID(entity, discogsId).catch(reason => findDiscogsRelatives(entity, discogsId).then(function(entries) {
console.assert(entries.length == 1, 'Ambiguous %s linkage for Discogs id', entity, discogsId, entries);
if (entries.length > 1) return params.searchSize > 0 ?
findMBIDByCommonTitles(entity, discogsId, entries.map(entry => entry.id))
: Promise.reject('Ambiguity');
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} found by having Discogs relative set`, 'salmon') });
saveToCache(entity, discogsId, entries[0].id);
return entries[0].id;
}));
if (params.searchSize > 0) promise = promise.catch(function(reason) {
if (/^(?:Ambiguity)\b/.test(reason)) return Promise.reject(reason);
if (!(entity in lookupIndexes) || !(discogsId in lookupIndexes[entity]))
return Promise.reject(`Assertion failed: ${entity}/${discogsId} not in lookup indexes`);
return mbApiRequest(entity, {
query: searchQueryBuilder(entity, discogsId),
limit: params.searchSize,
}).then(results => findMBIDByCommonTitles(entity, discogsId, results[(entity + 's').replace(/s(?=s$)/, '')].filter(function(result) {
if (result.score > 90) return true;
const equal = (name, normFn = str => str) => {
const cmp = root => similarStringValues(normFn(root.name), normFn(name));
return cmp(result) || result.aliases && result.aliases.some(cmp);
};
return equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name))
|| lookupIndexes[entity][discogsId].anv && equal(lookupIndexes[entity][discogsId].anv)
|| ['label', 'place'].includes(entity) && equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name),
entity => entity && entity.replace(...rxBareLabel));
}).map(result => result.id)));
});
return promise;
}
function translateDiscogsMarkup(source, richFormat = true) {
if (!source || !(source = source.trim())) return Promise.resolve(source);
const entryTypes = { a: 'artist', r: 'release', m: 'master', l: 'label', u: 'user' };
const mbEntity = key => ({ m: 'release-group' })[key] || entryTypes[key];
const nameNormalizer = name => name
&& stripDiscogsNameVersion(name.replace(/[\x00-\x1f]+/g, '').trim().replace(/\s+/g, ' '));
const link = (url, caption) => url && richFormat ? caption ?
`[${encodeURI(url)}|${caption.replace(/[\[\]\|]/g, m => `&#${m.charCodeAt(0)};`)}]`
: '[' + encodeURI(url) + ']' : caption;
return (function(body, replacer) {
if (typeof body != 'string' || typeof replacer != 'function') throw 'Invalid argument';
if (richFormat) {
body = body.replace(/\[(?![armlutg]=?\d+\]|url=|\/?url\]|img=)([^\[\]]*)\]/ig, '[$1]');
body = ['u', 'i', 'b'].reduce((str, tag, index) =>
str.replace(new RegExp(`\\[(${tag})\\]([\\S\\s]*?)\\[\\/\\1\\]`, 'ig'), (...m) =>
'"'.repeat(index) + m[2] + '"'.repeat(index)), body);
} else body = ['u', 'i', 'b'].reduce((str, tag, index) =>
str.replace(new RegExp(`\\[(${tag})\\]([\\S\\s]*?)\\[\\/\\1\\]`, 'ig'), '$2'), body);
body = body.replace(/\[([armlu])=([^\[\]\r\n]+)\]/ig,
(match, key, id) => !/^\d+$/.test(id) ? nameNormalizer(id) : match);
let lookupWorkers = [ ], match;
const entryExtractor = /\[([armlu])=?(\d+)\]/ig;
while ((match = entryExtractor.exec(body)) != 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);
}
return (lookupWorkers = lookupWorkers.map(function(entry) {
function addMethod(method) {
if (typeof method != 'function') return;
promise = promise instanceof Promise ? promise.catch(method) : method();
}
const discogsEntry = dcApiRequest(`${entryTypes[entry.key]}s/${entry.id}`).then(result => ({
key: entry.key, id: entry.id,
resolvedId: result.id,
caption: result.name ? nameNormalizer(result.name) : result.title.trim(),
}), function(reason) {
console.warn(`Discogs lookup for ${entry.key}${entry.id} failed:`, reason);
return null;
});
if (richFormat) {
const mbEntry = mbEntry => mbEntry ? {
key: entry.key, id: entry.id,
resolvedId: mbEntry.id,
caption: mbEntry.name || mbEntry.title,
} : null;
const getFromMBID = mbid => rxMBID.test(mbid) ?
mbApiRequest(mbEntity(entry.key) + '/' + mbid, { inc: 'url-rels' }).then(mbEntry)
: Promise.reject('Invalid MBID');
const findFromMBIDs = mbids =>
findMBIDByCommonTitles(mbEntity(entry.key), entry.id, mbids).then(getFromMBID);
if (['a', 'l'].includes(entry.key)) var promise = getCachedMBID(entryTypes[entry.key], entry.id)
.then(mbid => getFromMBID(mbid).catch(reason => discogsEntry));
if (['a', 'r', 'm', 'l'].includes(entry.key)) addMethod(() =>
findDiscogsRelatives(mbEntity(entry.key), entry.id).then(function(entries) {
if (entries.length > 1) return ['a', 'l'].includes(entry.key) && params.searchSize > 0 ?
findFromMBIDs(entries.map(entry => entry.id)) : Promise.reject('Ambiguity');
discogsEntry.then(discogsEntry => '<b>' + discogsEntry.caption + '</b>', reason => entry.id).then(name =>
{ notify(`MBID for ${entryTypes[entry.key]} ${name} found by having Discogs relative set`, 'salmon') });
if (['a', 'l'].includes(entry.key)) saveToCache(entryTypes[entry.key], entry.id, entries[0].id);
return mbEntry(entries[0]);
}));
if (['a', 'l'].includes(entry.key) && params.searchSize > 0) addMethod(reason => reason != 'Ambiguity (releases)' ?
discogsEntry.then(discogsEntry => mbApiRequest(mbEntity(entry.key), {
query: discogsEntry.caption,
limit: params.searchSize,
})).then(results => findFromMBIDs(results[mbEntity(entry.key) + 's'].map(result => result.id))) : Promise.reject(reason));
addMethod(() => discogsEntry);
} else promise = discogsEntry;
return promise.catch(function(reason) {
console.warn('Discogs entry lookup failed by all methods (', entry, ')');
return null;
});
})).length > 0 ? Promise.all(lookupWorkers).then(entries => (entries = entries.filter(Boolean)).length > 0 ?
Object.assign.apply({ }, Object.keys(entryTypes).map(key => ({ [key]: (function() {
const items = entries.filter(entry => entry.key == key).map(entry =>
({ [entry.id]: { caption: entry.caption, id: entry.resolvedId }}));
return items.length > 0 ? Object.assign.apply({ }, items) : { };
})() }))) : Promise.reject('No entries were resolved')).then(lookupTable =>
body.replace(entryExtractor, function(match, key, id) {
const entry = lookupTable[key = key.toLowerCase()][id = parseInt(id)];
if (!entry) console.warn('Discogs item not resolved:', match);
return entry ? replacer(key, entry.id, entry.caption) : replacer(key, id);
})) : Promise.resolve(body);
})(source, richFormat ? function replacer(key, id, caption) {
if (!key || !id) throw 'Invalid argument';
return link((mbEntity(key) && rxMBID.test(id) ? [mbOrigin, mbEntity(key), id]
: [dcOrigin, entryTypes[key], id]).join('/'), caption);
} : (key, id, caption) => caption || entryTypes[key] + id).catch(function(reason) {
console.warn(reason);
return source;
}).then(source => [
[/\[url=([^\[\]\r\n]+)\]([^\[\]\r\n]*)\[\/url\]/ig, function(m, url, caption) {
if (richFormat) try {
url = new URL(url.trim(), dcOrigin);
return link(url.href, caption);
} catch(e) { console.warn('Invalid Discogs link:', url) }
return caption || url.trim();
}], [/\[url\]([^\[\]\r\n]+)\[\/url\]/ig, function(m, url) {
if (richFormat) try {
url = new URL(url.trim(), dcOrigin);
return link(url.href);
} catch(e) { console.warn('Invalid Discogs link:', url) }
return url.trim();
}], [/\[img=([^\[\]\r\n]+)\]/ig, richFormat ? (m, url) => link(url.trim()) : '$1'],
[/\[t=?(\d+)\]/ig, richFormat ? `[${dcOrigin}/help/forums/topic?topic_id=$1]` : `${dcOrigin}/help/forums/topic?topic_id=$1`],
[/\[g=?([^\[\]\r\n]+)\]/ig, richFormat ? `[${dcOrigin}/help/guidelines/$1]` : `${dcOrigin}/help/guidelines/$1`],
[/[ \t]+$/gm, ''], [/(?:\r?\n){2,}/g, '\n\n'],
].reduce((str, substitution) => str.replace(...substitution), source));
}
function purgeArtists(fromIndex = 0) {
const artistSuffixes = ['mbid', 'name', 'artist.name', 'join_phrase'];
const key = (ndx, sfx) => `artist_credit.names.${ndx}.${sfx}`;
for (let ndx = 0; artistSuffixes.some(sfx => formData.has(key(ndx, sfx))); ++ndx)
artistSuffixes.forEach(sfx => { formData.delete(key(ndx, sfx)) });
}
function namedBy(entity, artist) {
const namedBy = (entity, artist) => new RegExp('\\b' + nameNorm(artist)
.replace(/[^\w\s]/g, '\\$&') + '\\b', 'i').test(nameNorm(entity));
const nb = (entity, artist) => {
if (namedBy(entity, artist.name)) return true;
//if (artist.namevariations && artist.namevariations.some(anv => namedBy(entity, anv))) return true;
if (artist.aliases && artist.aliases.some(alias => namedBy(entity, alias.name))) return true;
return false;
};
if (nb(entity.name, artist)) return true;
//if (entity.namevariations && entity.namevariations.some(anv => nb(anv, artist))) return true;
if (entity.aliases && entity.aliases.some(alias => nb(alias.name, artist))) return true;
return false;
}
if (!params.mbidLookup) params.searchSize = 0;
const relateAtLevel = sourceEntity => sourceEntity && ({
'work': params.workRelations,
'recording': params.recordingRelations,
'release': params.releaseRelations,
'release-group': params.rgRelations,
}[sourceEntity]);
const relateAtAnyLevel = ['work', 'recording', 'release', 'release-group'].some(relateAtLevel);
if (['recording', 'work'].some(relateAtLevel)) params.tracklist = true;
if (params.createMissingEntities) params.assignUncertain = true;
const rxMLang = /^(.+?)\s*=\s*(.+)$/, literals = { }, lookupIndexes = { }, openedForEdit = new Set;
const discogsName = (entity, discogsId) => (entity in lookupIndexes && discogsId in lookupIndexes[entity] ?
Promise.resolve(lookupIndexes[entity][discogsId].name)
: dcApiRequest(`${discogsEntity(entity)}s/${discogsId}`).then(discogsEntry =>
discogsEntry.name || discogsEntry.title, reason => entity + '#' + discogsId)).then(name => '<b>' + name + '</b>');
const matchNameVariant = (artist, nameVariant) => artist && nameVariant
&& (artist.name && sameStringValues(stripDiscogsNameVersion(artist.name), nameVariant)
|| artist.namevariations && artist.namevariations.some(anv => sameStringValues(anv, nameVariant)));
const nameNorm = name => name && toASCII(stripDiscogsNameVersion(name));
const hasType = (...types) => types.some(type => formData.getAll('type').includes(type));
const trackPosMapper = trackPos => (trackPos || '').toString()
.replace(/\d+/g, m => m.padStart(3, '0')).replace(/\W+/g, '').toUpperCase();
const noCreditAsArtists = [194];
formData.set('name', normSeedTitle(release.title));
//frequencyAnalysis(literals, release.title);
const released = dateParser(release.released), credits = { }, workers = [ ], rgLookupWorkers = [ ];
discogsCountryToIso3166Mapper(release.country).forEach(function(countryCode, countryIndex) {
if (countryCode) formData.set(`events.${countryIndex}.country`, countryCode);
if (released != null) {
function setDate(index, part) {
const key = `events.${countryIndex}.date.${part}`;
if ((index = released[index]) > 0) formData.set(key, index);
else formData.delete(key);
}
setDate(0, 'year'); setDate(1, 'month'); setDate(2, 'day');
}
});
const dynamicIdResolvers = Object.defineProperties({ }, { artist: { writable: false, value: class {
static get 451329() { return ['4d5447d7-c61c-4120-ba1b-d7f471d385b9', 'b0b33754-a664-43b7-ba00-be0dc4ec2396'] }; // John Lennon & Yoko Ono
static get 779927() { return ['4d5447d7-c61c-4120-ba1b-d7f471d385b9', 'ba550d0e-adac-4864-b88b-407cab5e76af'] }; // Lennon-McCartney
} }, label: { writable: false, value: class {
static get 1003() { // BMG
if (released) if (released[0] <= 2004) return '29d7c88f-5200-4418-a683-5c94ea032e38';
else if (released[0] >= 2008) return '82ef9b02-7b42-49fe-a6bc-0d8ba816d72f';
else return null;
};
static get 5870() { return null }; // Metronome ambiguous
static get 51167() { return null }; // Rough Trade ambiguous
static get 275182() { // Chem19
if (released) if (released[0] < 2005) return '32a3c0b8-e2b8-4b44-afe1-56389455aab4';
else if (released[0] >= 2005) return 'ef1f87b8-c502-41b8-9549-b21125feeec1';
else return null;
};
} }, place: { writable: false, value: class {
static get 265254() { // Albert Studios
if (released) if (released[0] < 1985) return '9a23510d-7902-4e53-a962-99ca058f1f83';
else if (released[0] >= 1985) return 'da44b506-e658-461f-8089-58f5a1b91b95';
else return null;
};
} } });
let defaultFormat, descriptors = new Set, media;
const relationsIndex = {
'artist': {
'work': {
'Songwriter': 167, 'Written By': 167, 'Written-By': 167, 'Author': 167,
'Composed By': 168, 'Music By': 168, 'Score': 168,
'Lyrics By': 165, 'Lyrics-By': 165, 'Words By': 165, 'Words-By': 165, 'Libretto By': 169,
'Translated By': 872, 'Translated-By': 872,
'Created By': 162, 'Created-By': 162, 'Transcription By': 162, 'Transcription-By': 162,
//'Arranged By': 293, 'Arranged-By': 293,
},
'recording': {
'Performer': 156, 'Guest': 156, 'Soloist': 156,
'Instruments': 148, 'Performer [Instruments]': 148, 'Musician': 148,
'Voice': 149, 'Vocals': 149, 'Alto Vocals': 149, 'Backing Vocals': 149, 'Baritone Vocals': 149,
'Bass Vocals': 149, 'Bass-Baritone Vocals': 149, 'Contralto Vocals': 149, 'Countertenor Vocals': 149,
'Harmony Vocals': 149, 'Lead Vocals': 149, 'Mezzo-soprano Vocals': 149, 'Solo Vocal': 149,
'Soprano Vocals': 149, 'Tenor Vocals': 149, 'Treble Vocals': 149, 'Whistling': 149, 'Choir': 149,
'Chorus': 149, 'Coro': 149, 'Caller': 149, 'Eefing': 149, 'Human Beatbox': 149, 'Humming': 149,
'Kakegoe': 149, 'MC': 149, 'Overtone Voice': 149, 'Rap': 149, 'Satsuma': 149, 'Scat': 149,
'Speech': 149, 'Narrator': 149, 'Proofreader': 149, 'Interviewee': 149, 'Interviewer': 149, 'Toasting': 149,
'Vocal Percussion': 149, 'Vocalese': 149, 'Yodeling': 149, 'Read By': 149, 'Commentator': 149,
'Dialog': 149, 'Voice Actor': 149, 'Shouts': 149,
'Orchestra': 150, 'Ensemble': 150, 'Band': 150, 'Backing Band': 150, 'Brass Band': 150, 'Concert Band': 150,
'Conductor': 151, 'Chorus Master': 152, 'Orchestrated By': 300, 'Concertmaster': 760, 'Concertmistress': 760,
'DJ Mix': 155, 'DJ-Mix': 155, 'Remix': 153, 'Mixed By': 143, 'Mixed-By': 143,
'Recorded By': 128, 'Recorded-By': 128, 'Adapted By': 297, 'Adapted-By': 297, 'Adapted By (Text)': 297,
'Arranged By': 297, 'Arranged-By': 297, 'Arranged By [Vocal]': 298,
'Art Direction': 137, 'Programmed By': 132, 'Programmed-By': 132, 'Sequenced By': 132, 'Sequenced-By': 132,
'Legal': 142, 'Booking': 134, 'Creative Director': 146, 'Engineer': 138, 'Engineer [Audio]': 140,
'Producer': 141, 'Produced By': 141,'Produced-By': 141, 'Co-producer': 141, 'Film Producer': 141,
'Executive Producer': 141, 'Executive-Producer': 141, 'Reissue Producer': 141, 'Post Production': 141,
'Editor': 144, 'A&R': 135,
'Engineer [Sound]': 133, 'Sound Engineer': 133, 'Engineer [Mix]': 143, 'Mix Engineer': 143, 'Mixing Engineer': 143,
'Engineer [Recording]': 128, 'Recording Engineer': 128, 'Engineer [Programming]': 132, 'Programming Engineer': 132,
'Engineer [Editor]': 144, 'Engineer [Balance]': 726, 'Balance Engineer': 726,
'Music Director': 1186, 'Music-Director': 1186, 'Audio Director': 1186, 'Directed By [Music Director]': 1186,
'Other': 129, 'Beats': 129, 'Cadenza': 129, 'Copyist': 129,
'Instrumentation By': 129, 'Musical Assistance': 129, 'Sound Designer': 129, 'Recording Supervisor': 129,
'Camera Operator': 129, 'Choreography': 129, 'Accompanied By': 129, 'Rhythm Section': 129,
/* video recording only */// 'Graphic Design': 125, 'Design': 130, 'Illustration': 130,
},
'release': {
'Performer': 51, 'Guest': 51, 'Soloist': 51,
'Instruments': 44, 'Performer [Instruments]': 44, 'Musician': 44,
'Voice': 60, 'Vocals': 60, 'Alto Vocals': 60, 'Backing Vocals': 60, 'Baritone Vocals': 60,
'Bass Vocals': 60, 'Bass-Baritone Vocals': 60, 'Contralto Vocals': 60, 'Countertenor Vocals': 60,
'Harmony Vocals': 60, 'Lead Vocals': 60, 'Mezzo-soprano Vocals': 60, 'Solo Vocal': 60,
'Soprano Vocals': 60, 'Tenor Vocals': 60, 'Treble Vocals': 60, 'Whistling': 60, 'Choir': 60,
'Chorus': 60, 'Coro': 60, 'Caller': 60, 'Eefing': 60, 'Human Beatbox': 60, 'Humming': 60,
'Kakegoe': 60, 'MC': 60, 'Overtone Voice': 60, 'Rap': 60, 'Satsuma': 60, 'Scat': 60,
'Speech': 60, 'Narrator': 60, 'Proofreader': 60, 'Interviewee': 60, 'Interviewer': 60, 'Toasting': 60,
'Vocal Percussion': 60, 'Vocalese': 60, 'Yodeling': 60, 'Read By': 60, 'Commentator': 60,
'Dialog': 60, 'Voice Actor': 60, 'Text By': 60, 'Shouts': 60,
'Orchestra': 45, 'Ensemble': 45, 'Band': 45, 'Backing Band': 45, 'Brass Band': 45, 'Concert Band': 45,
'Conductor': 46, 'Chorus Master': 53, 'Orchestrated By': 40, 'Concertmaster': 759, 'Concertmistress': 759,
'Compiled By': 48, 'Compiled-By': 48, 'Collected By': 48, 'Collected-By': 48,
'DJ Mix': 43, 'DJ-Mix': 43, 'Remix': 47, 'Mixed By': 26, 'Mixed-By': 26,
'Songwriter': 54, 'Written By': 54, 'Written-By': 54, 'Author': 54,
'Composed By': 55, 'Music By': 55, 'Score': 55,
'Lyrics By': 56, 'Lyrics-By': 56, 'Words By': 56, 'Words-By': 56, 'Libretto By': 57,
'Translated By': 871, 'Translated-By': 871, 'Arranged By': 295, 'Arranged-By': 295,
'Producer': 30, 'Produced By': 30, 'Produced-By': 30, 'Co-producer': 30, 'Film Producer': 30,
'Compilation Producer': 30, 'Executive Producer': 30, 'Executive-Producer': 30,
'Reissue Producer': 30, 'Post Production': 30, 'Recorded By': 36, 'Field Recording': 1012,
'Mastered By': 42, 'Mastered-By': 42, 'Remastered By': 42, 'Remastered-By': 42, 'Engineer': 28,
'Engineer [Audio]': 31, 'Engineer [Mastering]': 42, 'Mastering Engineer': 42,
'Engineer [Sound]': 29, 'Sound Engineer': 29, 'Engineer [Mix]': 26, 'Mix Engineer': 26, 'Mixing Engineer': 26,
'Engineer [Recording]': 36, 'Recording Engineer': 36, 'Engineer [Programming]': 37, 'Programming Engineer': 37,
'Programmed By': 37, 'Programmed-By': 37, 'Sequenced By': 37, 'Sequenced-By': 37,
'Engineer [Editor]': 38, 'Engineer [Balance]': 727, 'Balance Engineer': 727, 'Engineer [Transfer]': 1179, 'Transfer Engineer': 1179,
'Music Director': 1187, 'Audio Director': 1187, 'Directed By [Music Director]': 1187, 'Editor': 38, 'Edited By': 38, 'Edited-By': 38,
'Artwork': 993, 'Artwork By': 993, 'Cover': 993, 'Calligraphy': 993, 'Design Concept': 993,
'Graphics': 993, 'Layout': 993, 'Image Editor': 993, 'Lettering': 993, 'Lithography': 993,
'Logo': 993, 'Model': 993, 'Painting': 993, 'Sleeve': 993, 'Typography': 993, 'Art Direction': 18,
'Photography': 20, 'Photography By': 20, 'Design': 928, 'Film Director': 1185, 'Illustration': 927,
'Transferred By': 1179, 'Graphic Design': 27, 'Liner Notes': 24, 'Sleeve Notes': 24, 'Booklet Editor': 929, 'Booking': 23,
'Other': 25, 'Score Editor': 25, 'Hosted By': 25, 'Music Consultant': 25, 'Contractor': 25, 'Directed By': 25,
'Directed-By': 25, 'Leader': 25, 'Repetiteur': 25, 'Commissioned By': 25, 'Commissioned-By': 25,
'Curated By': 25, 'Curated-By': 25, 'Research': 25, 'Supervised By': 25, 'Supervised-By': 25,
'Animation': 25, 'Assemblage': 25, 'CGI Artist': 25, 'Cinematographer': 25, 'Costume Designer': 25,
'Director Of Photography': 25, 'Film Editor': 25, 'Film Technician': 25, 'Filmed By': 25,
'Footage By': 25, 'Gaffer': 25, 'Grip': 25, 'Hair': 25, 'Lighting': 25, 'Lighting Director': 25,
'Make-Up': 25, 'Production Manager': 25, 'Realization': 25, 'Screen Printing': 25, 'Set Designer': 25,
'Stage Manager': 25, 'Stylist': 25, 'Video Editor': 25, 'VJ': 25, 'Abridged By': 25, 'Booklet Editor': 25,
'Music Librarian': 25, 'Screenwriter': 25, 'Script By': 25, 'Script-By': 25,
'Text-By': 25, 'Administrator': 25, 'Advisor': 25, 'Consultant': 25, 'Coordinator': 25,
'Legal': 25, 'Management': 25, 'Product Manager': 25, 'Project Manager': 25, 'Promotion': 25,
'Public Relations': 25, 'Tour Manager': 25, 'Vocal Coach': 25, 'Authoring': 25, 'Crew': 25, 'DAW': 25,
'Direct Metal Mastering By': 25, 'Instrument Builder': 25, 'Lathe Cut By': 25, 'Lathe Designer': 25,
'Luthier': 25, 'Overdubbed By': 25, 'Plated By': 25, 'Restoration': 25, 'Tape Op': 25, 'Technician': 25,
'Tracking By': 25, 'Tuner': 25, 'Beats': 25, 'Cadenza': 25,
'Copyist': 25, 'Instrumentation By': 25, 'Musical Assistance': 25, 'Sound Designer': 25,
'Recording Supervisor': 25, 'Camera Operator': 25, 'Choreography': 25, 'Accompanied By': 25,
'Rhythm Section': 25, 'Created By': 25, 'Created-By': 25, 'Transcription By': 25, 'Transcription-By': 25,
'Lacquer Cut By': 969,
// 'Concept By': 25, 'Concept-By': 25,
},
'release-group': {
'A&R': 62, 'Creative Director': 63, 'Concept By': 63, 'Concept-By': 63,
},
},
'label': {
'work': { 'Published By': 208 },
'release': {
'Copyright (c)': 708, 'Phonographic Copyright (p)': 711,
'Licensed To': 833, 'Licensed By': 833, 'Licensed-By': 833, 'Licensed From': 712,
'Published By': 362, 'Published-By': 362, 'Distributed By': 361, 'Distributed-By': 361,
'Made By': 360 /* ?? */, 'Made-By': 360 /* ?? */, 'Manufactured By': 360, 'Manufactured-By': 360,
'Glass Mastered At': 955, 'Pressed By': 942, 'Pressed-By': 942, 'Printed By': 985, 'Printed-By': 985,
'Manufactured For': 952, 'Marketed By': 848, 'Marketed-By': 848, 'Produced For': 951,
'Duplicated By': 999 /* ?? */, 'Duplicated-By': 999 /* ?? */, 'Licensed Through': 999,
'Record Company': 999 /* ?? */, 'Recorded By': 999 /* ?? */, 'Exclusive Retailer': 999,
'Exported By': 999, 'Exported-By': 999,
},
},
'series': { 'release': { 'Part Of': 741 }, 'release-group': { 'Part Of': 742 } },
'place': {
'recording': {
'Recorded At': 693, 'Engineered At': 813, 'Mixed At': 694,
'Produced At': 825, 'Remixed At': 829, 'Filmed At': 963,
} ,
'release': {
'Recorded At': 695, 'Engineered At': 812, 'Mixed At': 696,
'Produced At': 824, 'Remixed At': 828,
'Mastered At': 697, 'Remastered At': 697, 'Arranged At': 865, 'Edited At': 820, 'Lacquer Cut At': 968,
'Transferred At': 1182, 'Manufactured At': 953, 'Glass Mastered At': 954, 'Pressed At': 941,
},
},
}, relationResolvers = { };
const cdFormats = {
'HD-?CD': 'HDCD',
'Enhanced': 'Enhanced CD',
'Copy Protected': 'Copy Control CD',
'CD\\+G': 'CD+G',
'DualDisc': 'DualDisc',
'SHM[ \\-]?CD': 'SHM-CD',
'(?:BS|Blu-?Spec)[ \\-]?CD2?': 'Blu-spec CD',
'HQ-?CD': 'HQCD',
'DTS[ \\-]?CD': 'DTS CD',
'Minimax CD': 'Minimax CD', // ?
'Mixed Mode CD': 'Mixed Mode CD', // ?
//'Hybrid': undefined,
};
if (release.formats) {
for (let format of release.formats) for (let description of getFormatDescriptions(format))
descriptors.add(description);
const hasFormat = (fmt, ...specifiers) => release.formats.some(format => format.name == fmt
&& (specifiers.length <= 0 || specifiers.every(specifier => getFormatDescriptions(format)
.some(RegExp.prototype.test.bind(new RegExp('^(?:' + specifier + ')$', 'i'))))));
if (hasFormat('Hybrid', 'DualDisc')) defaultFormat = 'DualDisc';
if (hasFormat('SACD', 'Hybrid')) defaultFormat = 'Hybrid SACD';
if (hasFormat('CDr')) defaultFormat = 'CD-R';
if (hasFormat('CD')) defaultFormat = 'CD';
for (let cdFormat in cdFormats) if (hasFormat('CD', cdFormat)) defaultFormat = cdFormats[cdFormat];
}
if (!defaultFormat) defaultFormat = 'CD';
descriptors = Array.from(descriptors);
processFormats({ // remove bogus tags
Stereo: undefined,
//Multichannel: undefined,
NTSC: undefined, PAL: undefined,
});
processFormats({
Album: 'Album',
EP: 'EP', 'Mini-Album': 'EP',
Single: 'Single', 'Maxi-Single': 'Single',
Compilation: 'Compilation', Sampler: 'Compilation',
Mixtape: 'Mixtape/Street',
Live: 'Live',
}, type => { formData.append('type', type) });
if (/ +\([^\(\)]*\b(?:live|(?:en|ao) (?:vivo|directo?))\b[^\(\)]*\)$/i.test(release.title))
formData.append('type', 'Live');
if (/ +\([^\(\)]*\b(?:soundtrack|score)\b[^\(\)]*\)$/i.test(release.title)
|| release.style && release.style.includes('Soundtrack'))
formData.append('type', 'Soundtrack');
const getRoles = artist => artist && (artist.roles || artist?.role.replace(/(?<=\[[^\[\]]+),(?=[^\[\]]+\])/g, '\x1E\x1F')
.split(',').map(role => role.trim().replace(/\x1E\x1F/g, ',')).filter(Boolean)) || [ ];
if (release.extraartists && release.extraartists.some(extraArtist => !extraArtist.tracks
&& getRoles(extraArtist).includes('DJ Mix'))) formData.append('type', 'DJ-mix');
else if (!hasType('DJ-mix') && !descriptors.includes('Mixed'))
if ('artist' in credits && 'DJ Mix' in credits.artist) descriptors.push('Mixed');
processFormats(cdFormats);
processFormats(Object.assign.apply({ },
['FLAC', 'MP[234]', 'OGG|Vorbis', 'AAC', 'M4[AB]', 'Opus', 'DSD\\d*']
.map(key => ({ [key]: undefined }))));
processFormats({
'Mini': '8cm',
'7"': undefined /*'Single'*/, '10"': undefined /*'Single'*/, '12"': undefined /*'EP'*/,
'LP': undefined,
}, size => { if (!defaultFormat.startsWith(size)) defaultFormat = size + ' ' + defaultFormat });
if (/^8cm (?!CD(?:\+G)?$)/.test(defaultFormat)) defaultFormat = defaultFormat.slice(4);
if (release.labels) release.labels.forEach(function(label, index) {
const prefix = 'labels.' + index;
if (label.name) {
formData.set(prefix + '.name', capitalizeName(stripDiscogsNameVersion(label.name)));
if (rxNoLabel.test(label.name) || release?.artists?.some(artist => namedBy(label, artist)))
formData.set(prefix + '.mbid', mb.spl.noLabel);
else addLookupEntry('label', label, prefix);
}
if (label.catno) formData.set(prefix + '.catalog_number',
rxNoCatno.test(label.catno) ? '[none]' : label.catno);
});
if (!Array.isArray(cdLengths) || cdLengths.length <= 0) cdLengths = false;
const rxParsingMethods = [/^()()(\S+)$/], rxRoleParser = /^(.+?)(?:\s+\[([^\[\]]+)\])?$/;
const totalMedia = release.formats ? release.formats.reduce((total, format) =>
total + (parseInt(format.qty) || 0), 0) : undefined;
if (totalMedia != 1) {
// most universal parser
rxParsingMethods.unshift(/^(?:([A-Z]{2,}|MP[234]|M4A)[\ \-\.]?)?(?:(\d+)[\ \-\.])?([A-Z]?\d+(?:\.(?:[a-z]|\d+))?)$/i);
// old parsers, just for sure
rxParsingMethods.push(/^([A-Z]{2,}|MP[234]|M4A)?(\d+)?[\ \-\.]?\b(\S+)$/i,
/^([A-Z]{2,}|MP[234]|M4A)(?:[\-\ ](\d+))?[\ \-\.]?\b(\S+)$/i);
}
const mediaSplitters = [media => layoutMatch(media) >= 0 ? media : undefined];
if (params.groupTracks) mediaSplitters.push(media => groupTracks(media, /^\S*?\d+/),
media => groupTracks(media, /^\S*\d+/), media => groupTracks(media));
if (params.alignWithTOCs) mediaSplitters.push(function alignWithTOCs(media) {
const cdMedia = media.filter(isCD);
if (cdMedia.length <= 0) return false;
const cdTracks = Array.prototype.concat.apply([ ], cdMedia.map(medium => medium.tracks));
if (cdTracks.length <= 0) return;
if (cdTracks.length != cdLengths.reduce((sum, totalTracks) => sum + totalTracks, 0)) return;
if (layoutMatch(media = cdLengths.map(function(discNumTracks, discIndex) {
const trackOffset = cdLengths.slice(0, discIndex).reduce((sum, totalTracks) => sum + totalTracks, 0);
const mediaIndex = Math.min(discIndex, cdMedia.length - 1);
return {
format: cdMedia[mediaIndex].format,
name: cdMedia[mediaIndex].name,
tracks: cdTracks.slice(trackOffset, trackOffset + discNumTracks)
.map((track, index) => Object.assign(track, { number: index + 1 })),
};
}).concat(media.filter(medium => !isCD(medium)))) > 2) return media;
}, function alignByTOCsIgnoreMedia(media) {
const cdTracks = Array.prototype.concat.apply([ ], media.map(medium => medium.tracks));
if (cdTracks.length <= 0) return;
if (cdTracks.length != cdLengths.reduce((sum, totalTracks) => sum + totalTracks, 0)) return;
if (layoutMatch(media = cdLengths.map(function(discNumTracks, discIndex) {
const trackOffset = cdLengths.slice(0, discIndex).reduce((sum, totalTracks) => sum + totalTracks, 0);
const mediaIndex = Math.min(discIndex, media.length - 1);
return {
format: defaultFormat,
name: media[mediaIndex].name,
tracks: cdTracks.slice(trackOffset, trackOffset + discNumTracks)
.map((track, index) => Object.assign(track, { number: index + 1 })),
};
})) > 2) return media;
});
if (params.tracklist && (mediaSplitters.some(function(mediaSplitter, splitterIndex) {
for (let collapseSubtracks of [false, true]) for (let rxParsingMethod of rxParsingMethods) {
media = parseTracklist(rxParsingMethod, collapseSubtracks);
if (!media || media.length <= 0) continue;
if (media = mediaSplitter(media)) return true;
}
return false;
}) || confirm('Could not find appropriatte tracks mapping to media (' +
(media = parseTracklist()).map(medium => medium.tracks.length).join('+') +
' ≠ ' + cdLengths.join('+') + '), attach tracks with this layout anyway?'))) {
for (let medium of media) {
if (medium.tracks.every((track, n, tracks) => track.heading && track.heading == tracks[0].heading)) {
medium.name = medium.tracks[0].heading;
for (let track of medium.tracks) track.heading = undefined;
} else if (medium.tracks[0].parents && medium.tracks.every((track, n, tracks) => track.parents && track.parents.length == tracks[0].parents.length
&& track.parents.every((parent, index) => tracks[0].parents[index] && parent.title == tracks[0].parents[index].title))) {
medium.name = medium.tracks[0].parents.map(parent => parent.title).filter(Boolean).join(' / ');
for (let track of medium.tracks) for (let parent of track.parents) delete parent.title;
}
}
(media = media.filter(isCD).concat(media.filter(medium => !isCD(medium)))).forEach(function(medium, mediumIndex) {
if (medium.format) formData.set(`mediums.${mediumIndex}.format`, medium.format);
if (medium.name) formData.set(`mediums.${mediumIndex}.name`, normSeedTitle(medium.name));
if (!medium.tracks) return;
const multilingual = medium.tracks.every(track => rxMLang.test(track.name));
medium.tracks.forEach(function(track, trackIndex) {
function addArtistCredits(trackArtist) {
if (trackArtist.extraartists) for (let extraArtist of trackArtist.extraartists)
addCredits(extraArtist.roles, extraArtist);
}
const prefix = `mediums.${mediumIndex}.track.${trackIndex}.`;
if (track.number) formData.set(prefix + 'number', track.number);
if (track.name) {
const _prefix = str => str ? str + ': ' : '';
const title = seedTitleNorm(multilingual ? track.name.replace(rxMLang, '$1') : track.name, formData);
formData.set(prefix + 'name', normSeedTitle(_prefix(track.heading) +
_prefix((track.parents || [ ]).map(parent => parent.title).filter(Boolean).join(' / ')) + title));
frequencyAnalysis(literals, track.name /*fullTitle*/);
}
if (track.trackArtists && track.trackArtists.length > 0) {
track.trackArtists.reduce(function(offset, trackArtist) {
if (offset > 0) formData.set(`${prefix || ''}artist_credit.names.${offset - 1}.join_phrase`, ' / ');
return seedArtists(trackArtist, prefix, offset);
}, 0);
track.trackArtists.forEach(addArtistCredits);
} else {
const trackArtist = resolveArtists(release, track);
if (!sameTrackArtists(trackArtist)) seedArtists(trackArtist, prefix);
if (debugLogging) console.debug('Resolved artists for track %d:', trackIndex + 1, trackArtist);
addArtistCredits(trackArtist);
}
if (track.length) formData.set(prefix + 'length', track.length);
});
});
}
if (release.extraartists) for (let extraArtist of release.extraartists)
addCredits(getRoles(extraArtist), extraArtist);
const relsBlacklist = ['Lacquer Cut By', 'Record Company'];
if (relateAtAnyLevel && 'artist' in credits) for (let role in credits.artist) {
const roleParser = rxRoleParser.exec(({
'Guitar [Electric]': 'Electric Guitar',
}[role]) || role);
if (roleParser == null || relsBlacklist.concat(['Featuring']).some(role =>
role.toLowerCase() == roleParser[1].toLowerCase())) continue;
const levels = findRelationLevels('artist', role);
if (levels.length <= 0 && !(role in relationResolvers)) relationResolvers[role] = (function(role) {
if (/\b(?:Band|Orchestra|Ensemble)$/.test(role[1])) return Promise.reject(role[0] + ' not resolved');
const levels = findRelationLevels('artist', role[1]);
if (levels.length > 0 && !levels.some(relateAtLevel)) return Promise.reject('Not to be related');
if (levels.length <= 0) return instrumentResolver(role[1]).then(attributes =>
instrumentMapper(attributes, role[1], role[2]), reason => [25/*, 129, 162*/]
.map(linkTypeId => ({ linkTypeId: linkTypeId, creditType: role[0] })));
if (['Instruments', 'Musician'].includes(role[1])) return instrumentResolver(role[2])
.catch(reason => null).then(attributes => instrumentMapper(attributes, role[2]));
return instrumentResolver(role[2]).catch(reason => null).then(instrument => levels.map(function(level) {
function testForAttribute(expr, appliesTo, attributeId) {
if (!(expr instanceof RegExp) || !Array.isArray(appliesTo) || !attributeId) throw 'Invalid argument';
if (!expr.test(role[2]) || !appliesTo.includes(relation.linkTypeId)) return false;
relation.attributes.push({ id: attributeId });
return true;
}
const instrumentRelIds = [
26, 28, 29, 30, 31, 36, 37, 38, 41, 44, 46, 49, 103, 105, 128, 132, 133, 138, 140, 141,
143, 144, 148, 151, 154, 158, 282, 726, 727, 798, 799, 800, 847, 923, 924, 986, 987,
], relation = { linkTypeId: relationsIndex.artist[level][role[1]], attributes: [ ] };
console.assert(relation.linkTypeId > 0, level, role[0]);
if (level == 'release-group' && /\b(?:re-?(?:master|issue)|edition\b)/i.test(role[2]))
relation.linkTypeId = 25;
if ([25, 129].includes(relation.linkTypeId))
relation.creditType = role[['Other'].includes(role[1]) ? 2 : 0];
else {
relation.creditType = role[1];
if (instrument && instrumentRelIds.includes(relation.linkTypeId)) relation.attributes.push(instrument);
else if (!testForAttribute(/^(?:Additional)\b/, [18, 19, 20, 22, 24, 26, 27, 28, 29, 30, 31, 36, 37, 38, 40, 41, 42, 44, 45, 46, 47, 49, 51, 53, 54, 55, 56, 57, 60, 63, 102, 103, 123, 125, 128, 130, 132, 133, 136, 137, 138, 140, 141, 143, 144, 146, 148, 149, 150, 151, 152, 153, 154, 156, 158, 164, 165, 167, 168, 169, 282, 293, 294, 295, 296, 297, 298, 300, 726, 727, 751, 871, 872, 927, 928, 993, 1179], '0a5341f8-3b1d-4f99-a0c6-26b7f4e42c7f')
&& !testForAttribute(/^(?:Assist(?:ed|ant))\b/, [18, 26, 28, 29, 30, 31, 36, 37, 38, 42, 46, 47, 53, 128, 132, 133, 136, 138, 140, 141, 143, 144, 151, 152, 153, 305, 726, 727, 856, 928, 962, 1165, 1179, 1185, 1186, 1187], '8c4196b1-7053-4b16-921a-f22b2898ed44')
&& !testForAttribute(/^(?:Associate)\b/, [26, 28, 29, 30, 31, 36, 37, 38, 41, 42, 128, 132, 133, 136, 138, 140, 141, 143, 144, 158, 282, 293, 294, 295, 296, 297, 298, 726, 727, 1179], '8d23d2dd-13df-43ea-85a0-d7eb38dc32ec')
&& !testForAttribute(/^(?:Guest|Feat(?:uring|\.)?)\b/, [44, 51, 60, 148, 149, 156, 305, 759, 760], 'b3045913-62ac-433e-9211-ac683cdf6b5c')
&& !testForAttribute(/^(?:Solo(?:ist)?)\b/, [44, 51, 60, 148, 149, 156], '63daa0d3-9b63-4434-acff-4977c07808ca')
&& !testForAttribute(/^(?:Executive)\b/, [28, 30, 138, 141], 'e0039285-6667-4f94-80d6-aa6520c6d359')
&& !testForAttribute(/^(?:Instrument)\b/, instrumentRelIds, '0abd7f04-5e28-425b-956f-94789d9bcbe2')
&& !testForAttribute(/^(?:Sub)\b/, [32, 161], '4521ce8e-3d24-4b64-9805-59df6f3a4740')
&& !testForAttribute(/^(?:Co)\b/, [26, 28, 29, 30, 31, 36, 38, 41, 42, 128, 133, 136, 138, 140, 141, 143, 144, 158, 282, 293, 294, 295, 296, 297, 298, 726, 727, 1179], 'ac6f6b4c-a4ec-4483-a04e-9f425a914573')
&& !testForAttribute(/^(?:Pre)\b/, [42], '288b973a-26ea-4880-8eca-45af4b8e8665')
&& !testForAttribute(/^(?:Translat(?:or$|ion\b|ed[ \-]By\b))/, [24], '25dfb08e-9b99-44db-b30c-1d6ec6747af8')
&& [20, 30, 62, 138, 141, 143, 993].includes(relation.linkTypeId)
&& (![30, 141].includes(relation.linkTypeId) || role[2] != 'Produced By'))
relation.attributes.push(taskAttribute(role[2]));
}
if (relation.attributes.length <= 0) relation.attributes = null;
return relation;
}));
})(roleParser);
if (levels.length > 0 ? levels.some(relateAtLevel) : role in relationResolvers)
for (let extraArtist of credits.artist[role]) addLookupEntry('artist', extraArtist, role);
}
if (release.series) for (let series of release.series) addCredit('series', 'Part Of', series);
if (release.companies) for (let company of release.companies) {
const entity = findRelationLevels('place', company.entity_type_name).length > 0 ? 'place' : 'label';
addCredit(entity, company.entity_type_name, company);
}
if (relateAtAnyLevel) for (let entity of ['label', 'series', 'place']) if (entity in credits) for (let type in credits[entity])
if (!relsBlacklist.includes(type) && (findRelationLevels(entity, type).some(relateAtLevel) || {
label: relateAtLevel('release'),
series: true,
}[entity])) for (let entry of credits[entity][type]) {
if (['label', 'place'].includes(entity) && ['artists', 'extraartists']
.some(prop => release[prop] && release[prop].some(artist => namedBy(entry, artist)))) continue;
addLookupEntry(entity, entry, type);
}
if (!media && release.tracklist) for (let track of release.tracklist)
if (track.type_ == 'track') frequencyAnalysis(literals, track.title);
if (Object.keys(literals).length > 0) guessTextRepresentation(formData, literals);
if (params.languageIdentifier && release.tracklist)
workers.push(languageIdentifier(release.tracklist.map(track =>
track.title.replace(...bracketStripper) + '.').join(' ')).then(function(result) {
/*if (!formData.has('language')) */formData.set('language', result.iso6393);
if (params.extendedMetadata) formData.set('language_en', result.language);
notify(`<b>${result.language}</b> identified as release language`, 'deeppink');
}, reason => { console.warn('Remote language identification failed') }));
const packagingMappers = {
[/^Book$/.source]: 'book',
[/^Box$|Box[ \-]?Set/.source]: 'box',
[/Jewel(?:[ \-]?Case)?/.source]: 'jewel case',
[/(?:Card(?:[ \-]?board)?|Paper)[ \-]?Sleeve/.source]: 'cardboard/paper sleeve',
[/Cassette(?: Case)?/.source]: undefined, //'cassette case',
[/Clamshell(?: Case)?/.source]: 'clamshell case',
[/Digi[ \-]?book(?: (?:Case|Cover))?/.source]: 'digibook',
[/Digi[ \-]?pac?k(?: (?:Case|Cover))?/.source]: 'digipak',
[/Digi[ \-]?(?:file|sleeve)(?: (?:Case|Cover))?/.source]: 'digifile',
[/Disc[ \-]?box(?: ?Slider)?/.source]: 'discbox slider',
[/Gatefold(?: Cover)?/.source]: 'gatefold cover',
[/Fat[ \-]?box(?: Case)?|^Fat(?:Box)?\b/.source]: 'fatbox',
[/^Keep$|Keep[ \-]?Case/.source]: 'keep case',
[/Long[ \-]?box(?: Case)?|Lbx/.source]: 'longbox',
[/Metal[ \-]?Tin(?: Case)?/.source]: 'metal tin',
[/Plastic[ \-]Sleeve/.source]: 'plastic sleeve',
[/Slide[ \-]?pac?k/.source]: 'slidepack',
[/Slim(?:[ \-]?line)? Jewel(?: Case)?/.source]: 'slim jewel case',
[/^Snap$|Snap[ \-]?Case/.source]: 'snap case',
[/Snap[ \-]?Pack/.source]: 'snappack',
[/Super[ \-]?Jewel(?: ?(?:Box|Case))?/.source]: 'super jewel box',
};
for (let key in packagingMappers) if (new RegExp('\\b(?:' + key + ')\\b', 'i').test(release.notes)
&& packagingMappers[key]) formData.set('packaging', packagingMappers[key]);
processFormats(packagingMappers, packaging => { formData.set('packaging', packaging) });
processFormats({
[/Unofficial(?: Release)?/.source]: 'bootleg',
[/Promo(?:tion(?:al)?)?/.source]: 'promotion',
[/Pseudo[ \-]Release/.source]: 'pseudo-release',
[/Withdrawn(?: Release)?/.source]: 'withdrawn',
[/Cancelled(?: Release)?/.source]: 'cancelled',
}, status => { formData.set('status', status) });
if (!formData.has('status')) formData.set('status', 'official');
if (formData.get('status') == 'official') {
if (/\b(?:cancelled)\b/i.test(release.notes)) formData.set('status', 'cancelled');
if (/\b(?:withdrawn)\b/i.test(release.notes)) formData.set('status', 'withdrawn');
}
descriptors = descriptors.map(function(descriptor) {
switch (descriptor) {
case 'Mixed': if (hasType('DJ-mix')) return false; else break;
case 'Remastered': if (hasType('Compilation')) return false; else break;
case 'Reissue': case 'Repress': case 'CD-TEXT': return false;
}
return descriptor.replace(...untitleCase).trim();
}).filter(Boolean);
if (descriptors.length > 0) formData.set('comment', descriptors.join(', ')); //else formData.delete('comment');
if (params.composeAnnotation) {
let annotation = [
!['release', 'release-group'].some(relateAtLevel) && release.series
&& release.series.map(discogsSeriesMapper).join('\n'),
release.notes && [
[/(?:\r?\n)*^\s*(?:(?:(?:Total|Running|Playing)\s+)+(?:Time|Length|Duration)|TT):? +(?:(?:\d+:)+\d+|(?:\d+['"] ?){2})$/gmi, ''],
[/(?:\r?\n){2,}/g, '\n\n'],
].reduce((str, subst) => str.replace(...subst), release.notes).trim(),
release.identifiers && release.identifiers
.filter(identifier => !['Barcode', 'ASIN'].includes(identifier.type))
.map(discogsIdentifierMapper).join('\n'),
].filter(Boolean);
if (annotation.length > 0) workers.push(translateDiscogsMarkup(annotation.join('\n\n'))
.then(annotation => { formData.set('annotation', annotation) }));
}
let urlRelIndex = -1, haveValidBarcode = false;
addUrlRef([dcOrigin, 'release', release.id].join('/'), 76);
if (relateAtLevel('release-group') && release.master_id > 0)
addUrlRef([dcOrigin, 'master', release.master_id].join('/'), 90);
if (release.identifiers) for (let identifier of release.identifiers) switch (identifier.type) {
case 'Barcode': {
function checkDigit(digits, effectiveLength = digits.length) {
const sum = digits.reduce((sum, digit, n) =>
sum + (n < effectiveLength ? digit * ((n & 1) == 0 ? 3 : 1) : 0), 0);
return (10 - sum % 10) % 10;
}
if (haveValidBarcode) break;
let barcode = identifier.value.replace(/\D+/g, '');
if (barcode.length < 9) break;
if (barcode.length > 13) console.warn('Nonstandard barcode:', barcode);
const digits = Array.from(barcode, char => parseInt(char)).filter(n => !isNaN(n));
switch (digits.length) {
case 11:
console.info('UPC check digit is', checkDigit(digits, 11),
'(', barcode + checkDigit(digits, 11), ')');
//barcode += checkDigit(digits, 11);
break;
case 12: case 13:
if (checkDigit(digits, digits.length - 1) == digits.pop()) {
haveValidBarcode = true;
console.info('Valid %s:', ['UPC', 'EAN'][digits.length - 11], barcode);
} else console.warn('Invalid %s:', ['UPC', 'EAN'][digits.length - 11], barcode);
break;
}
formData.set('barcode', barcode);
break;
}
case 'ASIN':
addUrlRef('https://www.amazon.com/gp/product/' + identifier.value, 77);
break;
}
purgeArtists();
seedArtists(release);
if (debugLogging) {
console.debug('Lookup indexes:', lookupIndexes);
console.debug('Credits table:', credits);
}
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from Discogs release id ${release.id} (${[dcOrigin, 'release', release.id].join('/')})`).trimLeft());
if (params.rgLookup && params.mbidLookup && !formData.has('release_group') && release.master_id > 0)
rgLookupWorkers.push(findDiscogsRelatives('release-group', release.master_id).then(function(releaseGroups) {
console.assert(releaseGroups.length > 0);
console.assert(releaseGroups.length == 1, 'Ambiguous master %d release referencing:', release.master_id, releaseGroups);
return releaseGroups.length == 1 ? releaseGroups[0] : Promise.reject('Ambiguity');
}).catch(reason => null));
if (params.extendedMetadata) {
let tags = (release.genres || [ ]).concat(release.styles || [ ]).filter(uniqueValues), tagIndex = -1;
for (let tag of (tags = tags.filter(uniqueValues))) formData.set(`tags.${++tagIndex}`, tag);
if (release.master_id) workers.push(dcApiRequest('masters/' + release.master_id).then(function(master) {
for (let tag of (master.genres || [ ]).concat(master.styles || [ ]).filter(uniqueValues))
if (!tags.includes(tag)) formData.set(`tags.${++tagIndex}`, tag);
}, console.error));
}
workers.push(getSessions(torrentId).catch(reason => null).then(function(sessions) {
function recordingsLookup(medium, track, mbidLookupFn) {
if (!track) throw 'Invalid argument'; else if (!media) return Promise.reject('Missing media');
if (!track.name) return Promise.reject('Missing track name');
const mediumIndex = media.indexOf(medium);
const trackIndex = medium && medium.tracks ? medium.tracks.indexOf(track) : -1;
console.assert(mediumIndex >= 0 && trackIndex >= 0);
if (layoutMatch(media) > 2) var trackLength = (function getLengthFromTOC() {
if (!sessions || !isCD(medium) || !(mediumIndex >= 0) || !(trackIndex >= 0)) return;
const tocEntries = getTocEntries(sessions[mediumIndex]);
if (tocEntries[trackIndex]) return (tocEntries[trackIndex].endSector + 1 -
tocEntries[trackIndex].startSector) * 1000 / 75;
})();
if (!(trackLength > 0)) trackLength = getTrackLength(track);
if (typeof mbidLookupFn != 'function') mbidLookupFn = undefined;
if (!(trackLength > 0) && !mbidLookupFn) return Promise.reject('Missing track length');
const maxLengthDifference = 5000;
const artists = trackMainArtists(track);
console.assert(artists != null, track);
return artists != null ? mbApiRequest('recording', {
query: (function queryBuilder() {
const OR = (values, mapFn) => Array.isArray(values)
&& (values = values.filter((recording, index, arr) => arr.indexOf(recording) == index)).length > 0
&& typeof mapFn == 'function' ? '(' + values.map(mapFn).join(' OR ') + ')' : '';
const fields = artists.map(function(artist) {
const arid = typeof mbidLookupFn == 'function' && mbidLookupFn('artist', artist.id);
return arid ? 'arid:' + arid : '(artistname:"' + stripDiscogsNameVersion(artist.name) +
'" OR creditname:"' + creditedName(artist) + '")';
});
fields.push(OR([track.name, track.name.replace(...bracketStripper)],
title => 'recording:"' + title + '"'));
if (trackLength > 0) fields.push(`(${[
`dur:[${Math.max(Math.round(trackLength) - 5000, 0)} TO ${Math.round(trackLength) + 5000}]`,
'(NOT dur:[* TO *])',
].join(' OR ')})`);
if (!canContainVideo(medium)) fields.push('video:false');
return fields.join(' AND ');
})(),
limit: 100,
}).then(function(recordings) {
function recordingValidator(recording, lengthRequired = false, dateRequired = false) {
if (recording.score < 90 || !canContainVideo(medium) && recording.video || [
'(?:re-?)mix(?:ed)?|RMX',
'live|(?:en|ao) (?:vivo|directo?)',
'clean|censored', 'karaoke', 'instrumental',
].some(function(pattern) {
const rx = new RegExp('\\b(?:' + pattern + ')\\b', 'i');
const remoteFlag = rxBracketStripper(undefined, pattern).test(recording.title)
|| rx.test(recording.disambiguation);
const localFlag = rxBracketStripper(undefined, pattern).test(track.name)
|| rxBracketStripper(undefined, pattern).test(release.title)
|| descriptors.some(RegExp.prototype.test.bind(rx));
return remoteFlag != localFlag;
}) || recording.releases && ['Live', 'Interview', 'Demo'].some(function(secondaryType) {
const releases = recording.releases.filter(release => 'release-group' in release);
if (releases.length <= 0) return false;
const count = releases.filter(release => 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType));
return hasType(secondaryType) ? count <= releases.length / 2 : count >= releases.length / 2;
}) || !Array.isArray(recording['artist-credit']) || !artists.every(function(artist) {
const arid = typeof mbidLookupFn == 'function' && mbidLookupFn('artist', artist.id);
return recording['artist-credit'].some(arid ? artistCredit =>
artistCredit.artist && artistCredit.artist.id == arid
: artistCredit => artistCredit.artist && matchNameVariant(artist, artistCredit.artist.name)
|| artistCredit.name && matchNameVariant(artist, artistCredit.name));
}) || dateRequired && !recordingDate(recording)) return false;
if (recording.length > 0 ? trackLength > 0 && deltaMapper(recording) > maxLengthDifference
: lengthRequired || !mbidLookupFn) return false;
return sameTitleMapper(recording, track.name, recordingDate(recording)
&& deltaMapper(recording) < 1000 ? weakMatchMapper : deltaMapper(recording) < 3000 ?
similarStringValues : sameStringValues);
}
const deltaMapper = recording => recording.length > 0 && trackLength > 0 ?
Math.abs(recording.length - trackLength) : NaN;
const weakMatchMapper = (...strings) => sameStringValues(...strings)
|| strings.some(str1 => strings.every(str2 => str2.toLowerCase().startsWith(str1.toLowerCase())))
|| strings.every(str => sameStringValues(...[str, strings[0]].map(str => str.replace(...bracketStripper))))
|| similarStringValues(strings[0], strings[1]);
if (recordings.count <= 0 || (recordings = recordings.recordings.filter(recording =>
recordingValidator(recording, false, false))).length <= 0) return Promise.reject('No matches');
return recordings.sort(function(...recordings) {
const hasLength = recording => recording.length > 0;
const cmpVal = fn => fn(recordings[0]) && !fn(recordings[1]) ? -1
: fn(recordings[1]) && !fn(recordings[0]) ? +1 : 0;
return [
function() {
if (!recordings.every(hasLength)) return;
const deltas = recordings.map(deltaMapper);
return deltas[0] < 1000 && deltas[1] >= 1000 || deltas[1] < 1000 && deltas[0] >= 1000
|| Math.abs(deltas[0] - deltas[1]) >= 1000 ? Math.sign(deltas[0] - deltas[1]) : 0;
}, () => recordings.every(recordingDate) ?
recordingDate(recordings[0]).localeCompare(recordingDate(recordings[1])) : 0,
() => cmpVal(recording => sameTitleMapper(recording, track.name)), function() {
if (!recordings.every(hasLength)) return;
const deltas = recordings.map(deltaMapper);
return Math.sign(deltas[0] - deltas[1]);
}, function() {
if (!recordings.every(recording => Array.isArray(recording.releases))) return;
const releases = recordings.map(recording => recording.releases.length);
return Math.sign(releases[1] - releases[0]);
}, () => cmpVal(recordingDate), () => cmpVal(hasLength),
].reduce((result, cmpFn) => result || cmpFn(...recordings), undefined) || 0;
});
}) : Promise.reject('No artists for track');
}
const recordingDate = recording => recording['first-release-date'] || recording.date;
const canContainVideo = medium => medium && medium.format
&& ['BLU-RAY', 'DVD'].includes(medium.format.toUpperCase());
const artistLookupWorkers = { };
if (params.lookupArtistsByRecording && !hasType('Live') && media && params.searchSize > 0)
for (let medium of media) if ('tracks' in medium) for (let track of medium.tracks) (function addArtistLookups(artists) {
if (artists != null) for (let artist of artists) {
if (!(artist.id in artistLookupWorkers)) artistLookupWorkers[artist.id] = [ ];
artistLookupWorkers[artist.id].push(recordingsLookup(medium, track).then(function(recordings) {
const mbids = [ ];
for (let recording of recordings) if (recording.score >= 90 && 'artist-credit' in recording)
for (let artistCredit of recording['artist-credit'])
if (artistCredit.artist && (matchNameVariant(artist, artistCredit.artist.name)
|| artistCredit.name && matchNameVariant(artist, artistCredit.name)))
mbids.push(artistCredit.artist.id);
return mbids.length > 0 ? mbids : null;
}).catch(reason => null));
}
})(trackMainArtists(track));
if (params.searchSize > 0) for (let discogsId in artistLookupWorkers)
artistLookupWorkers[discogsId] = Promise.all(artistLookupWorkers[discogsId]).then(function(mbids) {
const scores = { };
for (let _mbids of mbids.filter(Boolean)) for (let mbid of _mbids)
if (!(mbid in scores)) scores[mbid] = 1; else ++scores[mbid];
return Object.keys(scores).length > 0 ? scores : Promise.reject('No matches');
});
return Promise.all(Object.keys(lookupIndexes).map(entity => Promise.all(Object.keys(lookupIndexes[entity]).map(function(discogsId) {
function checkMBID(mbid) {
console.assert(rxMBID.test(mbid), mbid);
if (!rxMBID.test(mbid)) return Promise.reject('Invalid MBID');
if (entity == 'artist' && discogsId in artistLookupWorkers) artistLookupWorkers[discogsId].then(function(mbids) {
if (Object.keys(mbids).length > 1)
console.warn('MBID for artist', [dcOrigin, 'artist', discogsId].join('/'),
'can resolve to multiple entities:', printArtistMBIDs(mbids));
if (!Object.keys(mbids).includes(mbid))
console.warn('MBID for artist', [dcOrigin, 'artist', discogsId].join('/'),
'matching different entities:', printArtistMBIDs(mbids));
if (Object.keys(mbids).length > 1 || !Object.keys(mbids).includes(mbid)) {
beep.play();
if (params.openInconsistent)
openInconsistent(entity, discogsId, Object.keys(mbids), 'recordings');
}
});
return mbid;
}
function createEntity(entity, discogsId, recursive = true) {
function relationResolver(entity, relation) {
if (!relation) throw 'Invalid argument';
if (!relation.id) return Promise.reject('Invalid entry');
let worker = findMBID(entity, relation.id);
if (recursive) worker = worker.catch(reason => createEntity(entity, relation.id, false));
return worker;
}
function typeIdFromUrl(url, linkTypes, socialNetwork, otherDatabase, purchaseForDownload) {
if (!url) throw 'Invalid argument';
try { url = new URL(url).hostname } catch(e) { return -Infinity }
const urlMatch = domain => domain == url || url.endsWith('.' + domain);
const domain = linkTypes ? Object.keys(linkTypes).find(urlMatch) : undefined;
if (domain) return linkTypes[domain];
if (socialNetwork > 0 && [
'facebook.com', 'twitter.com', 'x.com', 'instagram.com', 'linkedin.com', ,'vk.com',
'tumblr.com', 'snapchat.com', 't.me', 'mixcloud.com',
].some(urlMatch)) return socialNetwork;
if (otherDatabase > 0 && [
'45cat.com', '45worlds.com', 'adp.library.ucsb.edu', 'anidb.net', 'animenewsnetwork.com',
'anison.info', 'baike.baidu.com', 'bibliotekapiosenki.pl', 'brahms.ircam.fr', 'cancioneros.si',
'cancioneros.si', 'castalbums.org', 'catalogue.bnf.fr', 'cbfiddle.com', 'ccmixter.org',
'ci.nii.ac.jp', 'classicalarchives.com', 'd-nb.info', 'dhhu.dk', 'discografia.dds.it',
'discosdobrasil.com.br', 'dr.loudness-war.info', 'dramonline.org', 'encyclopedisque.fr',
'ester.ee', 'finna.fi', 'finnmusic.net', 'folkwiki.se', 'fono.fi', 'generasia.com',
'goodreads.com', 'ibdb.com', 'id.loc.gov', 'idref.fr', 'imvdb.com', 'irishtune.info',
'isrc.ncl.edu.tw', 'iss.ndl.go.jp', 'japanesemetal.gooside.com', 'jaxsta.com',
'jazzmusicarchives.com', 'kbr.be', 'librarything.com', 'livefans.jp', 'lortel.org',
'mainlynorfolk.info', 'maniadb.com', 'metal-archives.com', 'mobygames.com', 'musicmoz.org',
'musik-sammler.de', 'muziekweb.eu', 'mvdbase.com', 'ocremix.org', 'offiziellecharts.de',
'openlibrary.org', 'operabase.com', 'operadis-opera-discography.org.uk', 'overture.doremus.org',
'pomus.net', 'progarchives.com', 'psydb.net', 'qim.com', 'rateyourmusic.com',
'residentadvisor.net', 'rock.com.ar', 'rockensdanmarkskort.dk', 'rockinchina.com',
'rockipedia.no', 'rolldabeats.com', 'smdb.kb.se', 'snaccooperative.org',
'soundtrackcollector.com', 'spirit-of-metal.com', 'spirit-of-rock.com', 'stage48.net',
'tedcrane.com', 'theatricalia.com', 'thedancegypsy.com', 'themoviedb.org', 'thesession.org',
'touhoudb.com', 'triplejunearthed.com', 'trove.nla.gov.au', 'tunearch.org', 'utaitedb.net',
'vkdb.jp', 'vndb.org', 'vocadb.net', 'whosampled.com', 'worldcat.org', 'www22.big.or.jp',
'www5.atwiki.jp', 'ra.co',
].some(urlMatch)) return otherDatabase;
if (purchaseForDownload > 0 && [
'beatport.com', 'qobuz.com', 'deezer.com', 'spotify.com', 'music.163.com',
].some(urlMatch)) return purchaseForDownload;
}
function normProfile(profile) {
profile = profile.trim().replace(/\s+/g, ' ');
if (profile.endsWith('.') && Array.prototype.filter.call(profile, ch => ch == '.').length < 2)
profile = profile.slice(0, -1);
return profile;
}
function extractIdentifiers(entry) {
if (!entry || !entry.profile) return;
let id, index = -1, rx = /\b(?:ISNI)(?::\s*|\s+)([\dX ]+)\b/g;
while ((id = rx.exec(entry.profile)) != null) {
if ((id = id[1].replace(/\s/g, '').slice(0, 16)).length == 16) {
let check = 11 - Array.prototype.reduce.call(id.slice(0, 15),
(sum, ch, index) => sum + parseInt(ch) * (16 - index), 0) % 11;
if (id[15] == (check > 9 ? 'X' : check.toString()))
postData.set(`edit-${entity}.isni_codes.${++index}`, id);
}
}
[index, rx] = [-1, /\b(?:IPI)(?::\s*|\s+)([A-Z]-\d{9}-\d)\b/g];
while ((id = rx.exec(entry.profile)) != null)
postData.set(`edit-${entity}.ipi_codes.${++index}`, id[1]);
}
function sortLegalName(name) {
if (!name || name.includes(',')) return name;
let words = name.split(/\s+/), sortName = words.pop();
if (words.length > 0) sortName += ', ' + words.join(' ');
return sortName;
}
function createAlias(entity, mbid, name, typeId, sortName) {
if (!entity || !mbid || !name) return Promise.reject('Invalid argument');
const postData = new URLSearchParams({ 'edit-alias.name': capitalizeName(name) });
if (typeId) postData.set('edit-alias.type_id', typeId);
if (sortName) postData.set('edit-alias.sort_name', capitalizeName(sortName));
if (scriptSignature) postData.set('edit-alias.edit_note', 'Auto-imported from Discogs by ' + scriptSignature);
if (!(params.createMissingEntities >= 2)) postData.set('edit-alias.make_votable', 1);
return globalXHR([mbOrigin, entity, mbid, 'add-alias'].join('/'), { responseType: null }, postData);
}
function isUrlPart(name, url) {
if (name && url) try {
const _url = new URL(url), normName = cmpNorm(name);
return ['hostname', 'pathname'].some(prop => cmpNorm(_url[prop]).includes(normName));
} catch(e) { console.warn(e) }
return false;
}
function processLabelProfile(entry) {
if (!entry.profile) return;
if (disambiguation = normProfile(entry.profile.trim().replace(/\r?\n[\S\s]*$/, '').trimRight()))
disambiguation = translateDiscogsMarkup(disambiguation, false);
if ((m = extractYear(entry, /\b(?:(?:est(?:\.|ablished\b)|founded\b|started\b|opened\b).{1,30}|(?:active (?:since|from)|created in)\b.{1,15})/.source)) > 0)
postData.set(`edit-${entity}.period.begin_date.year`, m);
if ((m = extractYear(entry, /\b(?:defunct(?: (?:since|from))?|ended|closed)\b.{1,15}/.source)) > 0) {
postData.set(`edit-${entity}.period.end_date.year`, m);
postData.set(`edit-${entity}.period.ended`, 1);
}
}
const postData = new URLSearchParams, edit = { }, workers = [ ];
const resolverAdapter = (resolver, postData) =>
resolver.then(mbid => Object.assign({ 'target': mbid }, postData));
let urlIndex = -1, disambiguation, m;
const create = entry => Promise.all(workers.map(worker =>
worker.catch(reason => null)).concat(disambiguation)).then(function(relationships) {
if (entry.name) postData.set(`edit-${entity}.name`, capitalizeName(stripDiscogsNameVersion(entry.name)));
if (edit.sort_name) postData.set(`edit-${entity}.sort_name`, edit.sort_name);
if (edit.type_id > 0) postData.set(`edit-${entity}.type_id`, edit.type_id);
if (edit.comment = relationships.pop()) {
while (edit.comment.length > 255) edit.comment = edit.comment.replace(/\s+\S+$/, '');
postData.set(`edit-${entity}.comment`, edit.comment);
} else postData.set(`edit-${entity}.comment`, '');
relationships.filter(Boolean).forEach(function(relationship, index) {
const requiredFields = ['link_type_id', 'target'].every(key => relationship[key]);
console.assert(requiredFields, relationship);
if (requiredFields) for (let key in relationship) if (relationship[key] != undefined)
postData.set(`edit-${entity}.rel.${index}.${key}`, relationship[key]);
});
if (scriptSignature) postData.set(`edit-${entity}.edit_note`, 'Auto-imported from Discogs by ' + scriptSignature);
if (!(params.createMissingEntities >= 2)) postData.set(`edit-${entity}.make_votable`, 1);
//if (debugLogging) console.debug('createEntity(%s):', Array.from(postData));
return globalXHR([mbOrigin, entity, 'create'].join('/'), undefined, postData).then(gidFromResponse).then(function(gid) {
saveToCache(entity, discogsId, gid);
if (postData.has(`edit-${entity}.name`)) {
const name = postData.get(`edit-${entity}.name`);
const ent = entity[0].toUpperCase() + entity.slice(1).toLowerCase();
notify(`${ent} <b>${name}</b> successfully created`, 'lime');
console.info(ent, name, 'successfully created (', gid, ')');
}
if (params.openCreatedEntries) {
if (!openedForEdit.has(gid)) {
openedForEdit.add(gid);
GM_openInTab([mbOrigin, entity, gid, 'edit'].join('/'), true);
}
if (params.openCreatedEntries >= 2) {
const url = new URL('search', mbOrigin);
let query = stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name);
if (['label', 'place'].includes(entity)) query = query.replace(...rxBareLabel);
url.searchParams.set('method', 'indexed');
url.searchParams.set('type', entity);
url.searchParams.set('query', query);
GM_openInTab(url.href, true);
}
}
return gid;
});
});
const extractYear = (entry, expr) => entry && entry.profile && expr
&& (entry = new RegExp(`(?:${expr})\\b((?:1\d|20)\\d{2})\\b`, 'i').exec(entry.profile)) != null
&& (entry = parseInt(entry[1])) >= 1000 && entry <= new Date().getUTCFullYear() ? entry : undefined;
const createHandler = ({
artist: artistId => dcApiRequest(discogsEntity(entity) + 's/' + artistId).then(function(artist) {
function addUrl(url) {
let linkTypeId = typeIdFromUrl(url, {
'musicbrainz.org': -1, 'myspace.com': -1, 'wikipedia.org': 179, 'wikidata.org': 352,
'discogs.com': 180, 'allmusic.com': 283, 'music.apple.com': 1131, 'bandcamp.com': 718,
'soundcloud.com': 291, 'music.youtube.com': 1080, 'last.fm': 840, 'vgmdb.net': 191,
'viaf.org': 310, 'setlist.fm': 816, 'imslp.org': 754, 'imdb.com': 178, 'cpdl.org': 981,
'cdbaby.com': 919, 'secondhandsongs.com': 307, 'purevolume.com': 174,
'youtube.com': 193, 'tiktok.com': 303, 'twitch.tv': 303, 'rumble.tv': 303,
}, 192, 188, 176);
if (!linkTypeId && /\b(?:blog)\b/i.test(url)) linkTypeId = 199;
if (!linkTypeId && isUrlPart(stripDiscogsNameVersion(artist.name.trim()), url))
linkTypeId = 183; // official homepage
if (linkTypeId < 0) return; else postData.set(`edit-${entity}.url.${++urlIndex}.text`, url);
if (linkTypeId != undefined) postData.set(`edit-${entity}.url.${urlIndex}.link_type_id`, linkTypeId);
else throw `Undetermined URL link type for ${entity} (${url})`;
}
const name = capitalizeName(stripDiscogsNameVersion(artist.name.trim())), rxs = [
/^(?:DJ|MC)\b|\b(?:Project)\b/,
/\b(?:group|band|ensemble|duo|trio|(?:quartet|quintet|sextet|septet|octet|nonet|tentet)(?:te?)?|all[ \-]?stars?|collective|gang|conjurito)\b|\s+(?:[\&\+]|and|vs\.?)\s+/i,
/\b(?:character)\b/i,
/\b(?:or(?:ch|k)est)/i,
/\b(?:choir|chorus|[ck]oro|singers)\b/i,
/^(The|Da|Le|La|El|Der|Die|Das|DJ|Dj|MC)\s+/,
/\b(?:Project)\b/i,
/^["„]?[']?\p{L}(?:[\-']?\p{L})*[']?\.?["“]?$/u,
/^[']?\p{L}(?:[\-']?\p{L})+[']?$/u,
];
if (rxs[3].test(name)) edit.type_id = 5; else if (rxs[4].test(name)) edit.type_id = 6;
else if (artist.members && artist.members.length > 1 || rxs[1].test(name)
|| rxs[1].test(artist.realname)) edit.type_id = 2;
else if (artist.realname || artist.groups && artist.groups.length > 0 || rxs[0].test(name))
edit.type_id = 1;
if ((m = rxs[5].exec(name)) != null && !/\s+(?:[\&\+]|and)\s+/i.test(name)) {
edit.sort_name = m.input.slice(m[0].length) + ', ' + m[1];
if (!edit.type_id) edit.type_id = -1;
} else if (edit.type_id == 1) edit.sort_name = sortLegalName(name);
else edit.sort_name = name;
if (!edit.type_id) {
const words = stripDiscogsNameVersion(artist.name.trim()).split(/\s+/);
if (words.length < 2 || words.length > 3 || words.every(word => word.toLowerCase() == word)
//|| words.some(word => /^[\p{L}]{2,}$/u.test(word) && word.toUpperCase() == word)
|| !words.every((word, n, a) => rxs[n < a.length - 1 ? 7 : 8].test(word)))
edit.type_id = -1;
}
if (!edit.type_id && rxs[6].test(name)) edit.type_id = -1;
if (!edit.type_id && params.openCreatedEntries) edit.type_id = -2;
if (!edit.type_id) return Promise.reject('Undeterminable sort name');
//if (edit.type_id == 1) postData.set(`edit-${entity}.gender_id`, 4); // 1=M. 2=F, 3=🤷, 4=not applicable, 5=other
addUrl([dcOrigin, discogsEntity(entity), artist.id].join('/'));
if (artist.urls) artist.urls.forEach(addUrl);
disambiguation = artist.profile ? [normProfile(artist.profile.trim())] : [ ];
if (artist.realname && (![2, 5, 6].includes(edit.type_id) || disambiguation.length <= 0)
&& artist.realname.toLowerCase() != stripDiscogsNameVersion(artist.name).toLowerCase())
disambiguation.unshift(artist.realname);
if (disambiguation = disambiguation.join('; ')) {
disambiguation = disambiguation.trim().replace(/\r?\n[\S\s]*$/, '').trimRight();
disambiguation = translateDiscogsMarkup(disambiguation, false);
} else if (artist.members && artist.members.length > 0) {
const members = artist.members.filter(member => member.active)
.map(member => stripDiscogsNameVersion(member.name));
if (members.length > 0) {
disambiguation = members.pop();
if (members.length > 0) disambiguation = members.join(', ') + ' & ' + disambiguation;
}
}
if (!disambiguation && artist.aliases && artist.aliases.length > 0)
disambiguation = 'aka. ' + artist.aliases.map(alias => stripDiscogsNameVersion(alias.name)).join(', ');
if (!disambiguation && artist.groups && artist.groups.length > 0) {
const groups = artist.groups.filter(group => group.active)
.map(group => stripDiscogsNameVersion(group.name));
if (groups.length > 0) disambiguation = 'member of ' + groups.join(', ');
}
if (artist.profile) {
if (!(edit.type_id > 4)) {
if (!(edit.type_id > 1) && (m = extractYear(artist, /\b(?:born\b.{1,30}|b\.\s*)/.source)) > 0
|| edit.type_id != 1 && (m = extractYear(artist, /\b(?:(?:est(?:\.|ablished\b)|founded\b|started\b).{1,30}|active (?:from|since)\b.{1, 15})/.source)) > 0)
postData.set(`edit-${entity}.period.begin_date.year`, m);
if (!(edit.type_id > 1) && (m = extractYear(artist, /\b(?:died|deceased)\b.{1,30}/.source)) > 0
|| edit.type_id != 1 && (m = extractYear(artist, /\b(?:dissolved|ended)\b.{1,15}/.source)) > 0) {
postData.set(`edit-${entity}.period.end_date.year`, m);
postData.set(`edit-${entity}.period.ended`, 1);
}
}
extractIdentifiers(artist);
}
const periodEnded = relative => 'active' in relative ? relative.active ? 0 : 1 : undefined;
if (artist.aliases) artist.aliases.forEach(function(alias) {
if (stripDiscogsNameVersion(alias.name) == stripDiscogsNameVersion(artist.name)) return;
if (sameStringValues(nameNorm(alias.name), nameNorm(artist.realname))) workers.push(resolverAdapter(relationResolver(entity, alias), {
'link_type_id': 108,
'backward': 1, // performance name of
'period.ended': periodEnded(alias),
})); else if (sameStringValues(nameNorm(artist.name), nameNorm(artist.realname))
|| params.createPerformsAsRels) workers.push(resolverAdapter(relationResolver(entity, alias), {
'link_type_id': 108,
'backward': 0, // performs as
'period.ended': periodEnded(alias),
}));
});
if (artist.members) artist.members.forEach(function(member) {
if (stripDiscogsNameVersion(member.name) == stripDiscogsNameVersion(artist.name)) return;
const mbidResolver = relationResolver(entity, member);
workers.push(resolverAdapter(mbidResolver, {
'link_type_id': 103,
'backward': 1,
'period.ended': periodEnded(member),
}));
if (namedBy(artist, member))
workers.push(resolverAdapter(mbidResolver, { 'link_type_id': 973, 'backward': 0 }),
resolverAdapter(mbidResolver, { 'link_type_id': 895, 'backward': 1 }));
});
if (artist.groups) artist.groups.forEach(function(group) {
if (stripDiscogsNameVersion(group.name) == stripDiscogsNameVersion(artist.name)) return;
const mbidResolver = relationResolver(entity, group);
workers.push(resolverAdapter(mbidResolver, {
'link_type_id': 103,
'backward': 0,
'period.ended': periodEnded(group),
}));
if (namedBy(group, artist))
workers.push(resolverAdapter(mbidResolver, { 'link_type_id': 973, 'backward': 1 }),
resolverAdapter(mbidResolver, { 'link_type_id': 895, 'backward': 0 }));
});
return create(artist).then(function(mbid) {
if (params.createAliases) {
const aliasWorkers = [ ];
if (params.createAliases >= 2 && artist.namevariations)
Array.prototype.push.apply(aliasWorkers, artist.namevariations
.filter(anv => anv.toLowerCase() != name.toLowerCase())
.map(anv => createAlias(entity, mbid, anv, 1)));
if (edit.type_id == 1 && artist.realname && artist.realname.toLowerCase() != name.toLowerCase())
aliasWorkers.push(createAlias(entity, mbid, artist.realname, 2, sortLegalName(artist.realname)));
if (aliasWorkers.length > 0) Promise.all(aliasWorkers).then(status =>
{ console.info(status.length, 'alias(es) successfully created for artist id', mbid) },
reason => { console.warn('Some aliases could not be created (%s)', reason) });
}
return mbid;
});
}),
label: labelId => dcApiRequest(discogsEntity(entity) + 's/' + labelId).then(function(label) {
function addUrl(url) {
let linkTypeId = typeIdFromUrl(url, {
'musicbrainz.org': -1, 'myspace.com': -1, 'wikipedia.org': 216, 'wikidata.org': 354,
'discogs.com': 217, 'apple.com': 1130, 'bandcamp.com': 719, 'soundcloud.com': 290,
'last.fm': 838, 'vgmdb.net': 210, 'imdb.com': 313, 'viaf.org': 311,
'secondhandsongs.com': 977,
'youtube.com': 225, 'tiktok.com': 304, 'twitch.tv': 304, 'rumble.com': 304,
}, 218, 222, 959);
if (!linkTypeId && isUrlPart(stripDiscogsNameVersion(label.name.trim()).replace(...rxBareLabel), url))
linkTypeId = 219; // official homepage
if (linkTypeId < 0) return; else postData.set(`edit-${entity}.url.${++urlIndex}.text`, url);
if (linkTypeId != undefined) postData.set(`edit-${entity}.url.${urlIndex}.link_type_id`, linkTypeId);
else throw `Undetermined URL link type for ${entity} (${url})`;
}
addUrl([dcOrigin, discogsEntity(entity), label.id].join('/'));
if (label.urls) label.urls.forEach(addUrl);
if (label.profile) {
processLabelProfile(label);
const id = /\b(?:LC):?\s*(\d+)\b/i.exec(label.profile);
if (id != null) postData.set(`edit-${entity}.label_code`, id[1]);
extractIdentifiers(label);
}
const labelAdapter = (relative, backward = false) =>
resolverAdapter(relationResolver(entity, relative),
{ 'link_type_id': 200, 'backward': backward ? 1 : 0 });
if (label.sublabels) Array.prototype.push.apply(workers,
label.sublabels.map(subLabel => labelAdapter(subLabel, false)));
if (label.parent_label) workers.push(labelAdapter(label.parent_label, true));
return create(label);
}),
series: seriesId => dcApiRequest(discogsEntity(entity) + 's/' + seriesId).then(function(series) {
function addUrl(url) {
let linkTypeId = typeIdFromUrl(url, {
'musicbrainz.org': -1, 'myspace.com': -1, 'wikipedia.org': 744, 'wikidata.org': 354,
'discogs.com': 747, 'viaf.org': 1001, 'setlist.fm': 938, 'soundcloud.com': 870,
'youtube.com': 792, 'tiktok.com': 805, 'twitch.tv': 805, 'rumble.com': 805,
}, 784, 746);
if (!linkTypeId && isUrlPart(stripDiscogsNameVersion(series.name.trim()), url))
linkTypeId = 745; // official homepage
if (linkTypeId < 0) return; else ++urlIndex;
postData.set(`edit-${entity}.url.${urlIndex}.text`, url);
if (linkTypeId != undefined) postData.set(`edit-${entity}.url.${urlIndex}.link_type_id`, linkTypeId);
else throw `Undetermined URL link type for ${entity} (${url})`;
}
if (series.parent_label) edit.type_id = 2;
if (!edit.type_id && series.profile
&& /\b(?:editions?\b|remasters?|re-?issues?|anniversary\b)/i.test(series.profile))
edit.type_id = 2;
if (!edit.type_id && params.openCreatedEntries && !['release', 'release-group'].some(relateAtLevel))
edit.type_id = 1;
if (!edit.type_id) return Promise.reject('Series type not determinable for ' + series.name);
postData.set(`edit-${entity}.ordering_type_id`, 1);
addUrl([dcOrigin, discogsEntity(entity), series.id].join('/'));
if (series.urls) series.urls.forEach(addUrl);
if (series.profile) {
disambiguation = normProfile(series.profile.trim().replace(/\r?\n[\S\s]*$/, '').trimRight());
if (disambiguation) disambiguation = translateDiscogsMarkup(disambiguation, false);
extractIdentifiers(series);
}
if (series.parent_label) workers.push(resolverAdapter(relationResolver('label', series.parent_label),
{ 'link_type_id': 933, 'backward': 0 }));
return create(series);
}),
place: placeId => dcApiRequest(discogsEntity(entity) + 's/' + placeId).then(function(place) {
function addUrl(url) {
let linkTypeId = typeIdFromUrl(url, {
'wikipedia.org': 595, 'wikidata.org': 594, 'musicbrainz.org': -1, 'myspace.com': -1,
'discogs.com': 705, 'viaf.org': 920, 'vgmdb.net': 1013, 'songkick.com': 787,
'setlist.fm': 817, 'last.fm': 837, 'imdb.com': 706, 'geonames.org': 934,
'bandsintown.com': 861, 'soundcloud.com': 940,
'youtube.com': 528, 'tiktok.com': 495, 'twitch.tv': 495, 'rumble.com': 495,
}, 429, 561);
if (!linkTypeId && isUrlPart(stripDiscogsNameVersion(place.name.trim()).replace(...rxBareLabel), url))
linkTypeId = 363; // official homepage
if (linkTypeId < 0) return; else postData.set(`edit-${entity}.url.${++urlIndex}.text`, url);
if (linkTypeId != undefined) postData.set(`edit-${entity}.url.${urlIndex}.link_type_id`, linkTypeId);
else throw `Undetermined URL link type for ${entity} (${url})`;
}
function guessPlaceType(value) {
if (!value) return;
if (/\b(?:(?:Studio|Estúdio)s?)\b/i.test(value)) return edit.type_id = 1;
if (/\b(?:Stadium)\b/i.test(value)) return edit.type_id = 4;
if (/\b(?:Arena)\b/i.test(value)) return edit.type_id = 5;
if (/\b(?:Park)\b/i.test(value)) return edit.type_id = 9;
if (/\b(?:Amphitheat(?:re|er))\b/i.test(value)) return edit.type_id = 43;
if (/\b(?:Hall|Theat(?:re|er))\b/i.test(value)) return edit.type_id = 44;
if (/\b(?:Club)\b/i.test(value)) return edit.type_id = 42;
if (/\b(?:Festival)\b/i.test(value)) return edit.type_id = 45;
if (/\b(?:Venue)\b/i.test(value)) return edit.type_id = 2;
}
if (place.name) guessPlaceType(stripDiscogsNameVersion(place.name));
addUrl([dcOrigin, discogsEntity(entity), place.id].join('/'));
if (place.urls) place.urls.forEach(addUrl);
if (place.profile) processLabelProfile(place);
if (place.contact_info) postData.set(`edit-${entity}.address`,
place.contact_info.split(/(?:\r?\n)+/).map(line => line.trim()).filter(Boolean).join(', '));
workers.push(resolverAdapter(findMBID('label', place.id), { 'link_type_id': 989, 'backward': 1 }));
return create(place);
}),
}[entity]);
console.assert(typeof createHandler == 'function', entity);
if (typeof createHandler != 'function') return Promise.reject('Create not implemented for ' + entity);
console.assert(discogsId in lookupIndexes[entity], lookupIndexes, entity, discogsId);
return mbApiRequest(entity, { query: searchQueryBuilder(entity, discogsId) }).then(function(results) {
results = results[({ series: 'series' }[entity]) || entity + 's'];
if (debugLogging) console.debug('Search results for %s %d:', entity, discogsId, results);
results = results.filter(function(result) {
const equal = (name, normFn = name => name && toASCII(name).toLowerCase()) => {
const cmp = root => normFn(root.name) == normFn(name);
return cmp(result) || result.aliases && result.aliases.some(cmp);
};
return equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name))
//|| lookupIndexes[entity][discogsId].anv && equal(lookupIndexes[entity][discogsId].anv)
|| ['label', 'place'].includes(entity) && equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name),
entity => entity && toASCII(entity.replace(...rxBareLabel)).toLowerCase());
});
if (debugLogging) console.debug('Search results for %s %d (filtered):', entity, discogsId, results);
return Promise.all(results.map(result => mbApiRequest(entity + '/' + result.id, { inc: `aliases+url-rels+${entity}-rels` }).then(function(entry) {
const discogsIds = getDiscogsRels(entry, discogsEntity(entity));
if (debugLogging) console.debug('Entry', entry.id, 'Discogs ids:', discogsIds, 'Relations:', entry.relations);
if (discogsIds.includes(parseInt(discogsId))) return discogsIds.length < 2 ? entry.id : true;
return discogsIds.length <= 0;
}).catch(reason => true)));
}).then(function(results) {
if (debugLogging) console.debug('Processed search results for %s %d:', entity, discogsId, results);
const discogsIds = results.filter(mbIdExtractor);
return discogsIds.length == 1 ? discogsIds[0] : results.filter(Boolean).length > 0 ?
Promise.reject('Name collision') : createHandler(discogsId);
});
}
const printArtistMBIDs = mbids => Object.keys(mbids).map(mbid =>
[mbOrigin, 'artist', mbid, 'recordings'].join('/') + ' => ' + mbids[mbid]);
let promise = findMBID(entity, discogsId);
if (entity == 'artist' && discogsId in artistLookupWorkers) promise = promise.catch(() =>
artistLookupWorkers[discogsId].then(function(mbids) {
const hiValue = Math.max(...Object.values(mbids));
if (Object.values(mbids).reduce((sum, count) => sum + count, 0) >= hiValue * 1.5) {
console.warn('MBID for artist', [dcOrigin, 'artist', discogsId].join('/'),
'resolved to multiple entities:', printArtistMBIDs(mbids), '(rejected)');
return Promise.reject('Ambiguity (recordings)');
} else if (Object.keys(mbids).length > 1) {
console.warn('MBID for artist', [dcOrigin, 'artist', discogsId].join('/'),
'resolved to multiple entities:', printArtistMBIDs(mbids), '(accepted)');
beep.play();
if (params.openInconsistent)
openInconsistent(entity, discogsId, Object.keys(mbids), 'recordings');
}
const mbid = Object.keys(mbids).find(key => mbids[key] == hiValue);
if (!mbid) return Promise.reject('Assertion failed: MBID indexed lookup failed');
if (debugLogging) console.debug('Entity binding found by matching existing recordings:',
[dcOrigin, discogsEntity(entity), discogsId].join('/') + '#' + discogsEntity(entity),
[mbOrigin, entity, mbid, 'releases'].join('/'));
notify(`MBID for ${entity} <b>${lookupIndexes[entity][discogsId].name}</b> found by match with <b>${hiValue}</b> existing recordings`, 'hotpink');
saveToCache(entity, discogsId, mbid);
return mbid;
}));
if (entity == 'artist') promise = promise.catch(reason => guessSPA(lookupIndexes[entity][discogsId].name));
if (params.createMissingEntities && params.searchSize > 0 && (['series'].includes(entity)
|| ['rg', 'release', 'recording', 'work'].some(entity => params[entity + 'Relations'])
|| !(entity in credits) || !lookupIndexes[entity][discogsId].contexts.every(context => context in credits[entity])))
promise = promise.catch(reason => getCachedMBID(entity, discogsId)
.catch(reason => createEntity(entity, discogsId, false)));
return promise.then(checkMBID).catch(function(reason) {
if (debugLogging) console.debug('%s %d finally not resolved with last reason:', entity, discogsId, reason);
return null;
});
})))).then(function(lookupResults) {
function getMBID(entity, discogsId) {
console.assert(entity in lookupIndexes);
if (!(entity in lookupIndexes)) return undefined;
let index = Object.keys(lookupIndexes[entity]).findIndex(key => parseInt(key) == discogsId);
return index >= 0 ? lookupResults[Object.keys(lookupIndexes).indexOf(entity)][index] : undefined;
}
let relIndex = 0, relationWorkers = [ ];
Object.keys(lookupIndexes).forEach(function(entity, ndx1) {
Object.keys(lookupIndexes[entity]).forEach(function(discogsId, ndx2) {
let mbids = lookupResults[ndx1][ndx2];
if (!mbids) return; else if (!Array.isArray(mbids)) mbids = [mbids];
lookupIndexes[entity][discogsId].contexts.forEach(function(context) {
if (!(entity in credits && context in credits[entity])) {
if (mbids.length == 1) formData.set(context + '.mbid', mbids[0]);
} else if (!relsBlacklist.includes(context)) mbids.forEach(function(mbid, mbidIndex) {
function addRelation(linkTypeId, attributes, { creditedAs, backward = false, extraData } = { }) {
console.assert(linkTypeId > 0);
const prefix = 'rel.' + relIndex++;
formData.set(prefix + '.entity', entity);
formData.set(prefix + '.link_type_id', linkTypeId);
if (backward) formData.set(prefix + '.backward', 1);
formData.set(prefix + '.target', mbid);
formData.set(prefix + '.name', stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name));
if (creditedAs) formData.set(prefix + '.credit', creditedAs);
if (attributes && (attributes = attributes.filter(attribute => attribute.id)).length > 0)
formData.set(prefix + '.attributes', JSON.stringify(attributes));
if (extraData) for (let key in extraData) formData.set(prefix + '.' + key, extraData[key]);
}
console.info('MBID for %s %s:', context, lookupIndexes[entity][discogsId].name, mbid);
switch (entity) {
case 'artist': {
const levels = findRelationLevels(entity, context);
const resolver = levels.length > 0 ? Promise.resolve(levels.map(level =>
({ linkTypeId: relationsIndex[entity][level][context] }))) : relationResolvers[context];
console.assert(resolver instanceof Promise, entity, context);
if (resolver instanceof Promise) relationWorkers.push(resolver.then(function(relations) {
function isCredited(track) {
function hasEA(root) {
const etraArtists = resolveExtraArtists([release, root],
role => role == context && !levels.some(isReleaseLevel));
return Boolean(etraArtists) && etraArtists.some(extraArtist =>
extraArtist.id == parseInt(discogsId) && extraArtist.roles.includes(context));
}
return levels.some(isTrackLevel) && (track.trackArtists && track.trackArtists.length > 0 ?
track.trackArtists.some(hasEA) : hasEA(track));
}
const levels = relations.map(relation => findRelationLevel(relation.linkTypeId));
const relateAtTrackLevel = levels.some(isTrackLevel) && Boolean(media)
&& media.some(medium => medium.tracks && medium.tracks.some(isCredited));
console.assert(relations.length > 0);
const debugLabel = `DiscogsID: ${discogsId}, context: ${context}`;
if (debugLogging) {
console.groupCollapsed(debugLabel);
console.debug('Lookup entry:', lookupIndexes[entity][discogsId], 'MBID:', mbid, 'Relations:', relations, 'Levels:', levels);
console.debug('Credit at track level:', relateAtTrackLevel);
}
for (let { linkTypeId, attributes, creditType = context } of relations) {
console.assert(linkTypeId > 0, relations);
if (!(linkTypeId > 0)) continue; else if (!attributes) attributes = [ ];
const sourceLevel = findRelationLevel(linkTypeId);
if (!relateAtLevel(sourceLevel) || isTrackLevel(sourceLevel) != relateAtTrackLevel) continue;
if ([25, 129, 162].includes(linkTypeId) || [20, 30, 62, 138, 141, 143, 993].includes(linkTypeId) && [
// Producer
'Post Production', 'Reissue Producer', 'Compilation Producer', 'Film Producer',
// Artwork
'Cover', 'Calligraphy', 'Design Concept', 'Graphics', 'Layout', 'Image Editor',
'Lettering', 'Lithography', 'Logo', 'Model', 'Painting', 'Sleeve', 'Typography',
].includes(creditType)) attributes.push(taskAttribute(creditType));
if ([26, 28, 29, 30, 31, 36, 38, 46, 49, 60, 103, 107, 128, 133, 138, 140, 141, 143, 144, 149, 151, 154, 294, 296, 298, 726, 727, 798, 799, 800].includes(linkTypeId)) switch (creditType) {
case 'Vocals': case 'Vocal': attributes.push({ id: 'd92884b7-ee0c-46d5-96f3-918196ba8c5b' }); break;
case 'Alto Vocals': attributes.push({ id: '9f63c4ba-b76f-40d5-9e99-2fb08bd4c286' }); break;
case 'Backing Vocals': attributes.push({ id: '75052401-7340-4e5b-a71d-ea024a128849' }); break;
case 'Baritone Vocals': attributes.push({ id: 'a40b43ed-2722-4b4a-98a5-478283cdf8df' }); break;
case 'Bass Vocals': attributes.push({ id: '1bfdb77e-f339-4e8e-9627-331ca9d9e920' }); break;
case 'Bass-Baritone Vocals': attributes.push({ id: '629763ee-3dc7-4225-b209-0ebb6d49bfab' }); break;
case 'Contralto Vocals': attributes.push({ id: '80d94f2e-e38f-4561-add2-c866f083d276' }); break;
case 'Countertenor Vocals': attributes.push({ id: '435a19f5-55dc-4a08-8c59-4257680b4217' }); break;
case 'Lead Vocals': attributes.push({ id: '8e2a3255-87c2-4809-a174-98cb3704f1a5' }); break;
case 'Mezzo-soprano Vocals': attributes.push({ id: 'f81325d7-593c-4197-b776-4f8a62c67a8e' }); break;
case 'Solo Vocal': attributes.push({ id: 'd92884b7-ee0c-46d5-96f3-918196ba8c5b' }, { id: '63daa0d3-9b63-4434-acff-4977c07808ca' }); break;
case 'Soprano Vocals': attributes.push({ id: 'e88f0be8-a07e-4c0d-bd06-e938eea4d5f6' }); break;
case 'Tenor Vocals': attributes.push({ id: '122c11da-651f-46cc-9118-c523a14afa1d' }); break;
case 'Treble Vocals': attributes.push({ id: '433631a2-68b7-49e6-90b4-5af19e26fc75' }); break;
case 'Whistling': attributes.push({ id: 'ed220196-6250-456d-ab7b-465bee605b16' }); break;
case 'Choir': case 'Chorus': case 'Coro': attributes.push({ id: '43427f08-837b-46b8-bc77-483453af6a7b' }); break;
case 'Speech': case 'Narrator': case 'Commentator': case 'Dialog': case 'Text By':
case 'Interviewer': case 'Interviewee': case 'Proofreader': case 'Read By':
case 'Voice Actor':
attributes.push({
id: 'd3a36e62-a7c4-4eb9-839f-adfebe87ac12',
creditedAs: creditType.replace(...untitleCase),
});
break;
case 'Caller': case 'Eefing': case 'Harmony Vocals': case 'Human Beatbox': case 'Humming':
case 'MC': case 'Overtone Voice': case 'Rap': case 'Satsuma': case 'Scat': case 'Toasting':
case 'Kakegoe': case 'Vocal Percussion': case 'Vocalese': case 'Yodeling': case 'Shouts':
attributes.push({
id: 'c359be96-620a-435c-bd25-2eb0ce81a22e',
creditedAs: creditType.replace(...untitleCase),
});
break;
}
if ([30, 141].includes(linkTypeId)) switch (creditType) {
case 'Executive-Producer': case 'Executive Producer': attributes.push({ id: 'e0039285-6667-4f94-80d6-aa6520c6d359' }); break;
case 'Co-producer': attributes.push({ id: 'ac6f6b4c-a4ec-4483-a04e-9f425a914573' }); break;
}
if (linkTypeId == 42 && ['Remastered By', 'Remastered-By'].includes(creditType)
|| linkTypeId == 697 && ['Remastered At', 'Remastered-At'].includes(creditType))
attributes.push({ id: '9b72452f-550e-4ace-93ed-fb8789cdc245' });
const extraArtist = credits[entity][context]
.find(extraArtist => extraArtist.id == parseInt(discogsId));
console.assert(extraArtist, credits[entity], context, discogsId);
const testForAttribute = (linkTypeIds, role) => linkTypeIds.includes(linkTypeId)
&& (creditType == role || extraArtist.modifiers && extraArtist.modifiers.includes(role));
if (testForAttribute([44, 51, 60, 148, 149, 156, 305, 759, 760], 'Guest'))
attributes.push({ id: 'b3045913-62ac-433e-9211-ac683cdf6b5c' });
if (testForAttribute([44, 51, 60, 148, 149, 156], 'Soloist'))
attributes.push({ id: '63daa0d3-9b63-4434-acff-4977c07808ca' });
const creditedAs = noCreditAsArtists.includes(parseInt(discogsId)) ? undefined
: capitalizeName(creditedName(lookupIndexes[entity][discogsId]));
if (debugLogging) console.debug('LinkTypeId:', linkTypeId, 'Attributes:', attributes, 'Credit type:', creditType);
if (relateAtTrackLevel) media.forEach(function(medium, mediumIndex) {
if (debugLogging) console.debug('Medium %d', mediumIndex + 1);
if (medium.tracks) medium.tracks.forEach(function(track, trackIndex) {
const _isCredited = isCredited(track);
if (debugLogging) console.debug('Track %d:', trackIndex + 1, track, 'is credited:', _isCredited);
if (_isCredited) addRelation(linkTypeId, attributes, {
creditedAs: creditedAs,
extraData: { medium: mediumIndex, track: trackIndex },
});
});
}); else {
const roleArtists = resolveExtraArtists([release], role => role == context);
const relateAtReleaseLevel = roleArtists && roleArtists
.some(roleArtist => roleArtist.id == parseInt(discogsId));
if (debugLogging) console.debug('Relate at release level:', relateAtReleaseLevel);
if (relateAtReleaseLevel) addRelation(linkTypeId, attributes, { creditedAs: creditedAs });
}
}
if (debugLogging) console.groupEnd(debugLabel);
}).catch(console.log));
break;
}
case 'label': case 'place': {
const addRelations = linkTypeIds => linkTypeIds.forEach(function(linkTypeId) {
console.assert(linkTypeId > 0, entity, context);
const level = findRelationLevel(linkTypeId);
console.assert(level, entity, linkTypeId);
if (relateAtLevel(level)) addRelation(linkTypeId,
[998, 999].includes(linkTypeId) ? [taskAttribute(context)] : null); // mbRelationsIndex[level][linkTypeId] == 'misc'
});
let levels = findRelationLevels(entity, context);
if (levels.length > 0) {
if (levels.some(isTrackLevel) && levels.some(isReleaseLevel))
levels = levels.filter(params.preferTrackRelations ? isTrackLevel : isReleaseLevel);
addRelations(levels.map(level => relationsIndex[entity][level][context]));
} else if (entity == 'label') addRelations([/*998, */999]);
break;
}
case 'series': {
const index = 'series/' + mbid;
if (!(index in relationResolvers)) relationResolvers[index] = mbApiRequest(index).then(function(mbSeries) {
let linkTypeId = ({
'52b90f1e-ff62-3bd0-b254-5d91ced5d757': 'release',
'4c1c4949-7b6c-3a2d-9d54-a50a27e4fa77': 'release-group',
}[mbSeries['type-id']]);
if (!linkTypeId && (linkTypeId = /^(.+?)\s+(?:series)$/.exec(mbSeries.type)) != null)
linkTypeId = linkTypeId[1].toLowerCase().replace(/[ _]+/g, '-');
if (linkTypeId && (linkTypeId = relationsIndex[entity][linkTypeId][context]) > 0)
return linkTypeId;
console.log('Unsupported series type for relating:', mbSeries.type);
return -1;
});
relationWorkers.push(relationResolvers[index].then(function(linkTypeId) {
if (relateAtLevel(findRelationLevel(linkTypeId))) {
const series = release.series.find(series => series.id == parseInt(discogsId));
console.assert(series, discogsId, release.series);
let number = series && series.catno
&& /^(?:\w+\.?\s+|[a-z]+\.)*\b(\d+|[IVXLCDM]+)$/i.exec(series.catno.trim());
if (number) number = /^(?:\d+)$/.test(number[1]) ? parseInt(number[1]) : romanToArabic(number[1]);
addRelation(linkTypeId, number ? [{
id: 'a59c5830-5ec7-38fe-9a21-c7ea54f6650a',
value: number.toString(),
}] : null, { backward: true });
} else if (!openedForEdit.has(mbid)) {
openedForEdit.add(mbid);
GM_openInTab([mbOrigin, entity, mbid, 'edit'].join('/'), true);
}
}).catch(console.warn));
break;
}
default: console.warn('Unexpected entity type:', entity);
}
});
});
});
});
if (params.rgLookup && !formData.has('release_group') && Array.isArray(release.artists)) {
function rgResolver(releaseGroups) {
if (!releaseGroups) return null;
const rgFilter = (releaseGroups, strictType = false, strictName = true) => releaseGroups.filter(function(releaseGroup) {
if (formData.has('type') && releaseGroup['primary-type']) {
const types = formData.getAll('type');
const cmpNocase = (...str) => str.every((s, n, a) => s.toLowerCase() == a[0].toLowerCase());
if (!types.some(type => cmpNocase(type, releaseGroup['primary-type']))) return false;
if (strictType && releaseGroup['secondary-types']) {
if (!releaseGroup['secondary-types'].every(secondaryType =>
types.some(type => cmpNocase(type, secondaryType)))) return false;
if (!types.every(type => cmpNocase(type, releaseGroup['primary-type'])
|| releaseGroup['secondary-types'].some(secondaryType =>
cmpNocase(secondaryType, type)))) return false;
}
}
return sameTitleMapper(releaseGroup, release.title, strictName ?
sameStringValues : similarStringValues, releaseTitleNorm)
|| releaseGroup.releases && releaseGroup.releases.some(release2 =>
sameTitleMapper(release2, release.title, strictName ?
sameStringValues : similarStringValues, releaseTitleNorm));
});
let filtered = rgFilter(releaseGroups, false, true);
if (filtered.length > 1) filtered = rgFilter(releaseGroups, true, true);
else if (filtered.length < 1) filtered = rgFilter(releaseGroups, false, false);
if (filtered.length != 1) filtered = rgFilter(releaseGroups, true, false);
return filtered.length == 1 ? filtered[0] : null;
}
Array.prototype.push.apply(rgLookupWorkers, release.artists.map(function(artist) {
if (artist.id == 194) return;
const mbid = getMBID('artist', artist.id);
if (mbid) return mbLookupById('release-group', 'artist', mbid).then(rgResolver, console.error);
}).filter(Boolean));
if (release.artists.length > 0) {
const normTitle = releaseTitleNorm(release.title);
rgLookupWorkers.push(mbApiRequest('release-group', { query: ['(' + [
`releasegroup:"${release.title}"`, `releasegroup:"${normTitle}"`,
`releasegroup:"${normTitle.replace(/(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]))+$/g, '')}"`,
`alias:"${release.title}"`, `alias:"${normTitle}"`,
`release:"${release.title}"`, `release:"${normTitle}"`,
].join(' OR ') + ')'].concat(release.artists.map(function(artist) {
const arid = getMBID('artist', artist.id);
return arid ? 'arid:' + arid : '(artistname:"' + stripDiscogsNameVersion(artist.name) +
'" OR creditname:"' + creditedName(artist) + '")';
})).join(' AND '), limit: 100 }).then(results => rgResolver(results['release-groups']), console.error));
}
}
return Promise.all(rgLookupWorkers).then(function releaseGroupResolver(releaseGroups) {
const releaseGroup = releaseGroups.find(Boolean);
if (releaseGroup) formData.set('release_group', releaseGroup.id); else return false;
let notification = `MBID for release group <b>${releaseGroup.name || releaseGroup.title}</b>`;
if (releaseGroup['first-release-date'])
notification += ` (<b>${getReleaseYear(releaseGroup['first-release-date'])}</b>)`;
notification += ` found by ${'relations' in releaseGroup ? 'unique name match' : 'known URL relation'}`;
notify(notification, 'goldenrod');
return true;
}).then(function findExistingRecordings() {
if (!(params.recordingsLookup > 0) || !media || !(params.recordingsLookup > 1)
&& !hasType('Single') && (formData.has('release_group') || hasType('DJ-mix', 'Remix', 'Live')
|| descriptors.some(RegExp.prototype.test.bind(/^(?:mix(?:ed|tape)|re-?mix(?:ed)?|RMX|rework)\b/i))))
return false;
return Promise.all(media.map(function(medium, mediumIndex) {
if (!medium || !Array.isArray(medium.tracks) || canContainVideo(medium))
return false;
return Promise.all(medium.tracks.map(function(track, trackIndex) {
if (params.recordingsLookup > 1 || !['DJ Mix', 'Remix', 'Mixed By'].some(function(role) {
const hasRole = trackArtist => trackArtist != null && trackArtist.extraartists
&& trackArtist.extraartists.some(extraArtist => getRoles(extraArtist).includes(role));
return track.trackArtists && track.trackArtists.length > 0 ?
track.trackArtists.some(hasRole) : hasRole(resolveArtists(release, track));
})) return recordingsLookup(medium, track, getMBID).then(function(recordings) {
if ((recordings = recordings.filter(recording => !/\b(?:live)\b/i
.test(recording.disambiguation))).length <= 0) return Promise.reject('No matches');
formData.set(`mediums.${mediumIndex}.track.${trackIndex}.recording`, recordings[0].id);
let notifyText = `MBID for recording <b>${track.name}</b> found`, firstRelease = [ ];
if (recordingDate(recordings[0])) firstRelease.push('<b>' +
getReleaseYear(recordingDate(recordings[0])) + '</b>');
if (recordings[0].releases && recordings[0].releases.length > 0) {
const release = recordings[0].releases.length > 1 ? recordings[0].releases.find(release =>
release.date == recordingDate(recordings[0])) : recordings[0].releases[0];
if (release) {
let releaseType = release['release-group'] && release['release-group']['primary-type'];
if (releaseType && releaseType.toUpperCase() != releaseType) releaseType = releaseType.toLowerCase();
if (releaseType && release['release-group']['secondary-types']
&& release['release-group']['secondary-types'].includes('Live'))
releaseType = 'live ' + releaseType;
firstRelease.push('on <b>' + (releaseType ? releaseType + ' ' + release.title : release.title) + '</b>');
}
}
if (firstRelease.length > 0) notifyText += ` (first released ${firstRelease.join(' ')})`;
notify(notifyText, 'orange');
if (debugLogging) console.debug('Closest recordings for track %o:', track, recordings);
}).catch(reason => { /*console.info('No recording for track %o found (%s)', track, reason)*/ });
}));
}));
}).then(() => Promise.all(relationWorkers));
});
}));
return Promise.all(workers).then(() => formData);
});
}
function seedFromAllMusic(formData, allMusicId, params, cdLengths) {
function getReleaseMeta(allMusicId) {
if (!allMusicId) throw 'Invalid argument';
const idExtractor = (url, entity) => url && entity
&& (entity = new RegExp(`\\bm${entity}\\d{10}\\b`, 'i').exec(url)) != null ? entity[0] : undefined;
return globalXHR(amOrigin + '/album/release/' + allMusicId).then(function(response) {
function coverResolver(element) {
if (element instanceof HTMLImageElement) try {
if (!(element = new URL(element.src)).pathname.includes('/images/no_image/')) {
element.searchParams.set('f', 0);
return element.href;
}
} catch(e) { console.warn(e) }
}
function trackListingAdapter(body) {
if (!(body instanceof HTMLBodyElement)) return null;
const media = Array.from(body.querySelectorAll('div#trackListing div.disc'), function(disc) {
const medium = {
title: textMapper(disc.querySelector(':scope > h3')),
tracks: Array.from(disc.querySelectorAll(':scope > div.track'), track => ({
trackNum: textMapper(track.querySelector('div.trackNum')),
title: textMapper(track.querySelector('div.title > a:first-of-type')),
artists: Array.from(track.querySelectorAll('div.performer > a'), artistMapper),
featArtists: Array.from(track.querySelectorAll('span.featuring > a'), artistMapper),
composers: Array.from(track.querySelectorAll('div.composer > a'), artistMapper),
duration: textMapper(track.querySelector('div.duration')),
})),
};
if (medium.title && !(medium.title = medium.title.replace(/^Disc\s+\d+(?:\s*[:\-])?\s*/, '')))
medium.title = undefined;
return medium.tracks.length > 0 ? medium : null;
});
return media.filter(Boolean).length > 0 ? media : null;
}
function creditsAdapter(body) {
if (!(body instanceof HTMLBodyElement)) return null;
const credits = { artists: [ ], featArtists: [ ], extraArtists: { } };
body.querySelectorAll('table.creditsTable > tbody > tr').forEach(function(tr) {
const artists = Array.from(tr.querySelectorAll('td.singleCredit > span > a'), artistMapper);
if (artists.length <= 0) return;
let artistCredits = textMapper(tr.querySelector('span.artistCredits')) || 'Primary Artist';
artistCredits = artistCredits.split(',').map(artist => artist.trim().toLowerCase());
for (let artistCredit of artistCredits) switch (artistCredit) {
case 'primary artist': Array.prototype.push.apply(credits.artists, artists); break;
case 'featured artist': Array.prototype.push.apply(credits.featArtists, artists); break;
default:
if (!(artistCredit in credits.extraArtists)) credits.extraArtists[artistCredit] = [ ];
Array.prototype.push.apply(credits.extraArtists[artistCredit], artists);
}
});
return credits;
}
function urlResolver(elem) {
if (elem instanceof HTMLElement) try { return new URL(elem.getAttribute('href'), amOrigin).href }
catch(e) { console.warn(e) }
}
const textMapper = elem => elem instanceof Element ? elem.textContent.trim() : undefined;
const artistMapper = elem => elem instanceof Element ? {
id: idExtractor(elem.href, 'n'),
name: textMapper(elem),
url: urlResolver(elem),
} : undefined;
const ajaxAdapter = url => globalXHR(url, { headers: { Referer: response.finalUrl } })
.then(({document}) => document ? document.body : null, function(reason) {
console.warn(reason);
return null;
});
const reviewAdapter = body => body instanceof HTMLBodyElement ? body.querySelector('div#review') : null;
const body = response.document.body, release = {
id: idExtractor(response.finalUrl, 'r'),
title: textMapper(body.querySelector('h1#releaseTitle')),
artists: Array.from(body.querySelectorAll('div#releaseHeadline > h2 > a'), artistMapper),
date: textMapper(body.querySelector('div#basicInfoMeta > div.releaseDate > span')),
format: textMapper(body.querySelector('div#basicInfoMeta > div.format > span')),
labels: Array.from(body.querySelectorAll('div#basicInfoMeta > div.label a'), artistMapper),
catalogNumber: textMapper(body.querySelector('div#basicInfoMeta > div.catalogNumber > span')),
genres: Array.from(body.querySelectorAll('div#basicInfoMeta > div.genre a'), textMapper),
styles: Array.from(body.querySelectorAll('div#basicInfoMeta > div.styles a'), textMapper),
releaseTypes: textMapper(body.querySelector('div#basicInfoMeta > div.releaseInfo > div')),
recordingDate: textMapper(body.querySelector('div.recording-date > div')),
recordingLocations: Array.from(body.querySelectorAll('div#basicInfoMeta > div.recordingLocation > div:not([id])'), textMapper),
cover: coverResolver(body.querySelector('div#releaseCover img')),
url: response.finalUrl,
};
let mainAlbum = body.querySelector('div#mainAlbumMeta a');
if (mainAlbum != null) mainAlbum = urlResolver(mainAlbum);
mainAlbum = mainAlbum ? Promise.all([
globalXHR(mainAlbum).then(response => ({
id: idExtractor(response.finalUrl, 'w'),
url: response.finalUrl,
title: textMapper(response.document.body.querySelector('h1#albumTitle')),
artists: Array.from(response.document.body.querySelectorAll('h2#albumArtists > a'), artistMapper),
date: textMapper(response.document.body.querySelector('div#basicInfoMeta > div.release-date > span')),
genres: Array.from(response.document.body.querySelectorAll('div#basicInfoMeta > div.genre a'), textMapper),
styles: Array.from(response.document.body.querySelectorAll('div#basicInfoMeta > div.styles a'), textMapper),
recordingDate: textMapper(response.document.body.querySelector('div.recording-date > div')),
recordingLocations: Array.from(response.document.body.querySelectorAll('div#basicInfoMeta > div.recording-location > div:not([id])'), textMapper),
cover: coverResolver(response.document.body.querySelector('div#albumCover img')),
})).catch(function(reason) {
console.warn(reason);
return null;
}),
ajaxAdapter(mainAlbum + '/trackListingAjax').then(trackListingAdapter),
ajaxAdapter(mainAlbum + '/creditsAjax').then(creditsAdapter),
ajaxAdapter(mainAlbum + '/reviewAjax').then(reviewAdapter),
]).then(([mainAlbum, media, artistCredits, review]) => mainAlbum && Object.assign(mainAlbum, {
media: media,
artistCredits: artistCredits,
review: review,
})) : Promise.resolve(null);
let [trackListingAjax, creditsAjax, reviewAjax] = ['trackListingAjax', 'creditsAjax', 'reviewAjax']
.map(ajax => ajaxAdapter(response.finalUrl + '/' + ajax));
trackListingAjax = trackListingAjax.then(trackListingAdapter);
creditsAjax = creditsAjax.then(creditsAdapter);
reviewAjax = reviewAjax.then(reviewAdapter);
if (release.releaseTypes && (release.releaseTypes = release.releaseTypes.split(/\r?\n/)
.map(releaseType => releaseType.trim()).filter(Boolean)).length <= 0)
release.releaseTypes = undefined;
return Promise.all([release, mainAlbum, trackListingAjax, creditsAjax, reviewAjax]);
}).then(([release, mainAlbum, media, artistCredits, review]) => Object.assign(release, {
mainAlbum: mainAlbum,
media: media,
artistCredits: artistCredits,
review: review,
}));
}
if (formData && typeof formData == 'object' && allMusicId) params = Object.assign({
tracklist: true,
mbidLookup: true, recordingsLookup: true, lookupArtistsByRecording: true, rgLookup: true,
searchSize: GM_getValue('mbid_search_size', 30),
languageIdentifier: GM_getValue('external_language_id', true),
composeAnnotation: GM_getValue('compose_annotation', true),
openInconsistent: GM_getValue('open_inconsistent', true),
assignUncertain: GM_getValue('assign_uncertain', false),
extendedMetadata: false, rgRelations: false, releaseRelations: false,
recordingRelations: false, workRelations: false, preferTrackRelations: false,
}, params); else throw 'Invalid argument';
const amOrigin = 'https://www.allmusic.com';
return getReleaseMeta(allMusicId).then(function(release) {
function addLookupEntry(entity, entry, context) {
console.assert(entity && entry && context);
if (!entity || !entry || !context) throw 'Invalid argument';
console.assert(entry.id && entry.name, entry);
if (!entry.id || !entry.name) return;
if (!(entity in lookupIndexes)) lookupIndexes[entity] = { };
if (!(entry.id in lookupIndexes[entity]))
lookupIndexes[entity][entry.id] = { name: entry.name, contexts: [ ] };
if (!lookupIndexes[entity][entry.id].contexts.includes(context))
lookupIndexes[entity][entry.id].contexts.push(context);
}
function addCredit(entity, context, entry, modifiers) {
if (!entity || !context || !entry) throw 'Invalid argument';
if (!(entity in credits)) credits[entity] = { };
if (!(context in credits[entity])) credits[entity][context] = [ ];
if (credits[entity][context].some(_entry => _entry.id == entry.id)) return;
const _entry = { id: entry.id, name: entry.name };
if (modifiers && modifiers.length > 0) _entry.modifiers = modifiers;
credits[entity][context].push(_entry);
}
function findRelationLevels(entity, type) {
if (!(entity in relationsIndex)) return [ ];
let levels = Object.keys(relationsIndex[entity]).filter(level => type in relationsIndex[entity][level]);
return levels;
}
function resolveExtraArtists(track, roleTrackEvaluator = role => !findRelationLevels('artist', role).some(isReleaseLevel)) {
function addRole(role, artists) {
if (!params.preferTrackRelations && !roleTrackEvaluator(role)) return;
if (role && artists) for (let artist of artists) {
let extraArtist = extraArtists.find(extraArtist => extraArtist.id == artist.id);
if (!extraArtist) extraArtists.push(extraArtist = { id: artist.id, name: artist.name, roles: [ ] });
if (!extraArtist.roles.includes(role)) extraArtist.roles.push(role);
}
}
console.assert(typeof roleTrackEvaluator == 'function', roleTrackEvaluator);
const extraArtists = [ ];
if (release.artistCredits.extraArtists) for (let role in release.artistCredits.extraArtists)
addRole(role, release.artistCredits.extraArtists[role]);
if (track && track.composers.length > 0) addRole('composer', track.composers);
if (extraArtists.length > 0) return extraArtists;
}
function seedArtists(root, prefix, offset = 0) {
function seedArtist(artist, index, array, prefix, offset = 0) {
prefix = `${prefix || ''}artist_credit.names.${offset + index}`;
formData.set(`${prefix}.artist.name`, capitalizeName(artist.name));
formData.delete(`${prefix}.name`);
if (index < array.length - 1)
formData.set(`${prefix}.join_phrase`, index < array.length - 2 ? ', ' : ' & ');
else formData.delete(`${prefix}.join_phrase`);
addLookupEntry('artist', artist, prefix);
}
if (!root) throw 'Invalid argument';
if (!Array.isArray(root.artists) || root.artists.length <= 0) return offset;
root.artists.forEach((artist, index, array) => { seedArtist(artist, index, array, prefix, offset) });
offset += root.artists.length;
if (!root.featArtists || root.featArtists.length <= 0) return offset;
formData.set(`${prefix || ''}artist_credit.names.${offset - 1}.join_phrase`, fmtJoinPhrase('feat.'));
root.featArtists.forEach((featArtist, index, array) =>
{ seedArtist(featArtist, index, array, prefix, offset) });
return offset += root.featArtists.length;
}
function addUrlRef(url, linkType) {
formData.set(`urls.${++urlRelIndex}.url`, url);
if (linkType != undefined) formData.set(`urls.${urlRelIndex}.link_type`, linkType);
}
function searchQueryBuilder(entity, allMusicId) {
function addField(field, expr) {
if (!(field in query)) query[field] = [ ];
query[field].push(expr);
}
if (!(entity in lookupIndexes) || !(allMusicId in lookupIndexes[entity])) return;
const query = { }, name = lookupIndexes[entity][allMusicId].name;
for (let field of [entity, 'alias', 'sortname']) addField(field, '"' + name + '"');
switch (entity) {
case 'label': {
const bareName = labelMapper(name.replace(...rxBareLabel));
for (let field of [entity, 'alias']) addField(field, '"' + bareName + '"');
break;
}
}
return Object.keys(query).map(field => query[field].map(expr => `${field}:(${expr})`).join(' OR ')).join(' OR ');
}
function layoutMatch(media) {
if (!media) return -Infinity; else if (!Array.isArray(cdLengths) || cdLengths.length <= 0) return 0;
if ((media = media.filter(medium => ['CD', 'CD-R'].includes(mediumFormat(medium)))).length != cdLengths.length) return -2;
if (media.every((medium, mediumIndex) => medium.tracks.length == cdLengths[mediumIndex])) return 3;
if (media.every((medium, mediumIndex) => medium.tracks.length == cdLengths[mediumIndex]
|| medium.format == 'Enhanced CD' && medium.tracks.length > cdLengths[mediumIndex])) return 2;
if (cdLengths.length > 1) {
const index = { };
for (let key of cdLengths) if (!(key in index))
index[key] = media.filter(medium => medium.tracks.length == key).length;
if (Object.keys(index).every(key1 => index[key1] == cdLengths.filter(key2 =>
key2 == parseInt(key1)).length)) {
notify('Tracks layout matched to reordered logs', 'orangered');
return 1;
}
}
return -1;
}
function openInconsistent(entity, allMusicId, mbids, subpage) {
Array.from(mbids).reverse().forEach(mbid =>
{ GM_openInTab([mbOrigin, entity, mbid, subpage].filter(Boolean).join('/'), true) });
GM_openInTab([amOrigin, amEntity(entity), allMusicId].join('/'), true);
}
function saveToCache(entity, allMusicId, mbid) {
amBindingsCache[entity][allMusicId] = mbid;
GM_setValue('allmusic_to_mb_bindings', amBindingsCache);
}
function getCachedMBID(entity, allMusicId, mbEntity = entity) {
if (!amBindingsCache) {
if (!(amBindingsCache = GM_getValue('allmusic_to_mb_bindings'))) amBindingsCache = { };
else console.info('AllMusic to MB bindings cache loaded:', Object.keys(amBindingsCache).map(key =>
`${Object.keys(amBindingsCache[key]).length} ${(key + 's').replace(/s(?=s$)/, '')}`).join(', '));
GM_addValueChangeListener('allmusic_to_mb_bindings',
(name, oldVal, newVal, remote) => { if (remote) amBindingsCache = newVal });
}
if (!(entity in amBindingsCache)) amBindingsCache[entity] = { };
if (!(allMusicId in amBindingsCache[entity])) return Promise.reject('Not cached');
if (!rxMBID.test(amBindingsCache[entity][allMusicId])) return Promise.resolve(null);
return globalXHR(`${mbOrigin}/${entity}/${amBindingsCache[entity][allMusicId]}`, {
method: 'HEAD', redirect: 'follow', anonymous: true,
}).then(function(response) {
if (response.status < 200 || response.status >= 400) return Promise.reject(response.statusText);
response = mbIdExtractor(response.finalUrl, mbEntity);
if (!response) return Promise.reject('Cached check failed');
console.log('Entity binding for', entity, allMusicId, 'got from cache');
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} got from cache`, 'sandybrown') });
if (response != amBindingsCache[entity][allMusicId]) {
console.info('MB entry for %s %d has moved: %s => %s',
entity, allMusicId, amBindingsCache[entity][allMusicId], response);
saveToCache(entity, allMusicId, response);
}
return response;
}).catch(function(reason) {
console.warn('Failed to verify %s MBID %s (%s)', entity, amBindingsCache[entity][allMusicId], reason);
return amBindingsCache[entity][allMusicId];
});
}
function findMBIDByCommonTitles(entity, allMusicId, mbids) {
function resolveUrl(elem) {
if (elem instanceof HTMLElement) try {
return new URL(elem.getAttribute('href'), amOrigin).href;
} catch(e) { console.warn(e) } else throw 'Invalid argument';
}
if (!entity || !allMusicId || !Array.isArray(mbids)) throw 'Invalid argument';
if (mbids.length <= 0) return Promise.reject('No MusicBrainz entries');
const amLoadSection = (channel, selector, itemMapper) => (function loadPage(page = 1) {
if (!channel || !selector || typeof itemMapper != 'function') throw 'Invalid argument';
const path = [amOrigin, amEntity(entity), allMusicId, channel + 'Ajax', 'all'];
if (!['compositions'].includes(channel)) path.push(page);
return globalXHR(path.join('/'), {
headers: { Referer: [amOrigin, amEntity(entity), allMusicId].join('/') },
}).then(function({document}) {
if (!document) return null;
const tracks = Array.from(document.body.querySelectorAll(selector), itemMapper).filter(Boolean);
const nextPage = document.body.querySelector('button.paginationNext');
return nextPage == null || nextPage.disabled ? tracks : loadPage(page + 1).then(nextTracks =>
Array.isArray(nextTracks) && nextTracks.length > 0 ? tracks.concat(nextTracks) : tracks);
});
})().catch(console.error);
const textAdapter = elem => elem instanceof HTMLElement ? elem.textContent.trim() : undefined;
const intAdapter = elem => elem instanceof HTMLElement && parseInt(elem.textContent) || undefined;
const artistAdapter = elem => elem instanceof HTMLElement ? ({
id: allMusicIdExtractor(resolveUrl(elem), 'artist'),
name: textAdapter(elem),
url: resolveUrl(elem),
}) : undefined;
const lookupMethods = [{
worker: mbGetReleasesAdapter(entity),
resolver: results => Promise.all([
globalXHR([amOrigin, amEntity(entity), allMusicId, 'discographyAjax'].join('/'), {
headers: { Referer: [amOrigin, amEntity(entity), allMusicId].join('/') },
}).then(({document}) => document && Promise.all(Array.from(document.body.querySelectorAll('select#releaseType > option[value]'), option => option.value).filter(releaseType => releaseType != 'all').map((releaseType, index) => globalXHR([amOrigin, amEntity(entity), allMusicId, 'discographyAjax', releaseType].join('/'), {
headers: { Referer: [amOrigin, amEntity(entity), allMusicId].join('/') },
}).then(({document}) => Array.from(document.body.querySelectorAll('table.discographyTable > tbody > tr'), function(album) {
const elems = ['td.year', 'td.meta > span.title a:first-of-type']
.map(selector => album.querySelector(selector));
if (elems[1] != null) return {
id: allMusicIdExtractor(resolveUrl(elems[1]), 'album')
|| allMusicIdExtractor(resolveUrl(elems[1]), 'album/release'),
year: intAdapter(elems[0]),
title: textAdapter(elems[1]),
type: elems[1].pathname.startsWith('/album/release/') ? 'release'
: elems[1].pathname.startsWith('/album/') ? 'album' : 'unknown',
url: resolveUrl(elems[1]),
releaseType: releaseType,
relationType: releaseType == 'compilations' ? 'track_artist' : 'artist',
};
}).filter(Boolean)))).then(releases => Array.prototype.concat.apply([ ], releases))).catch(console.error),
amLoadSection('credits', 'table.creditsTable > tbody > tr', function(album) {
const elems = [
'td.creditYear',
'td.creditMeta span.album > a:first-of-type',
'td.creditMeta span.credits',
].map(selector => album.querySelector(selector));
if (elems[1] != null) return {
id: allMusicIdExtractor(resolveUrl(elems[1]), 'album')
|| allMusicIdExtractor(resolveUrl(elems[1]), 'album/release'),
year: intAdapter(elems[0]),
artists: Array.from(album.querySelectorAll('td.creditMeta span.artists > a'), artistAdapter),
title: textAdapter(elems[1]),
type: elems[1].pathname.startsWith('/album/release/') ? 'release'
: elems[1].pathname.startsWith('/album/') ? 'album' : 'unknown',
url: resolveUrl(elems[1]),
relationType: elems[2] != null && elems[2].textContent.toLowerCase().split(',').map(credit =>
credit.trim().replace(/^(?:primary artist)$/, 'track_artist')).join(', ') || undefined,
};
}),
]).then(albums => Array.prototype.concat.apply([ ], albums.filter(Boolean))).then(function(amAlbums) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'), true);
GM_openInTab([amOrigin, amEntity(entity), allMusicId].join('/'), true);
}
if (!amAlbums || amAlbums.length <= 0) return Promise.reject('No matches by common releases');
const mutualScores = results.map(results => results ? results.reduce(function(score, result) {
const relatedAlbums = [ ];
switch (result.level) {
case 'release':
for (let allMusicId of getAllMusicRels(result['release-group'], 'album'))
Array.prototype.push.apply(relatedAlbums, amAlbums.filter(amAlbum => amAlbum.id == allMusicId));
break;
case 'release_group':
for (let allMusicId of getAllMusicRels(result, 'album'))
Array.prototype.push.apply(relatedAlbums, amAlbums.filter(amAlbum => amAlbum.id == allMusicId));
break;
default: console.warn('Unexpected result level:', result);
}
if (relatedAlbums.length > 0) {
console.assert(relatedAlbums.length < 2, relatedAlbums);
if (debugLogging) console.debug('Found matching releases by existing relation:', result, relatedAlbums);
return score + 1;
} else return score + Math.max(...amAlbums.map(function(amAlbum) {
function titleSimilarity(root) {
if (root) if (sameTitleMapper(root, amAlbum.title, sameStringValues))
return root.title.length;
else if (sameTitleMapper(root, amAlbum.title, sameStringValues, releaseTitleNorm))
return releaseTitleNorm(root.title).length;
return 0;
}
const releaseGroup = result.level == 'release_group' ? result : result['release-group'];
const primaryType = releaseGroup && releaseGroup['primary-type'];
if (primaryType && (amAlbum.releaseType == 'singles')
!= (['Single', 'EP'].includes(primaryType))) return 0;
const secondaryTypes = releaseGroup && releaseGroup['secondary-types'];
if (secondaryTypes && (amAlbum.releaseType == 'compilations')
!= ['Compilation'].some(secondaryType => secondaryTypes.includes(secondaryType))) return 0;
const q = [0, 0];
const releaseYear = result.level == 'release' ? getReleaseYear(result.date) : NaN;
const rgYear = releaseGroup && getReleaseYear(releaseGroup['first-release-date']) || NaN;
if (!(amAlbum.year > 0)) return 0; else if (amAlbum.type == 'album') {
if (amAlbum.year == rgYear) q[0] = 1; else if (amAlbum.year <= releaseYear) q[0] = 1/2;
} else if (amAlbum.type == 'release') {
if (amAlbum.year == releaseYear) q[0] = 1; else if (amAlbum.year >= rgYear) q[0] = 1/2;
}
if (!(q[0] > 0)) return 0;
if (amAlbum.type == 'album') q[1] = titleSimilarity(releaseGroup);
else if (amAlbum.type == 'release' && result.level == 'release') q[1] = titleSimilarity(result);
if (!(q[1] > 0)) return 0;
let score = q[0] * ((base, confidencyLen, exp = 1, factor = 1) =>
base + Math.pow(Math.min(q[1], confidencyLen) / confidencyLen, exp) * factor * (1 - base))
(0, 5, 0.75, 0.80);
if (entity == 'artist' && (result.relationType == 'track_artist'
|| amAlbum.relationType == 'track_artist')) score *= 2/3;
if (debugLogging) console.debug('Found matching releases:', result, amAlbum, 'Score:', score);
return score;
}));
}, 0) : 0);
const hiScore = Math.max(...mutualScores);
if (debugLogging) console.debug('Common titles lookup method #1: Entity:', entity,
'AllMusic ID:', allMusicId, 'MBIDs:', mbids, 'Scores:', mutualScores, 'HiScore:', hiScore);
if (!(hiScore > 0)) return Promise.reject('No matches by common releases');
const hiIndex = mutualScores.indexOf(hiScore);
console.assert(hiIndex >= 0, hiScore, mutualScores);
if (debugLogging && hiIndex < 0) alert('HiIndex not found! (see the log)');
const dataSize = Math.min(amAlbums.length, results[hiIndex].length);
if (!(Math.pow(hiScore, 3) * 10 >= Math.min(dataSize, 10) && hiScore * 50 >= dataSize))
if (params.assignUncertain) openUncertain();
else return Promise.reject('Matched by common releases with too low match rate');
else if (hiScore < 1) openUncertain();
console.log('Entity binding found by having score %f (%d):\n%s\n%s', hiScore, dataSize,
[amOrigin, amEntity(entity), allMusicId].join('/'),
[mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'));
if (mutualScores.filter(score => score > 0).length > 1) {
console.log('Matches by more entities:', mutualScores.map((score, index) =>
score > 0 && [mbOrigin, entity, mbids[index], 'releases'].join('/') + ' (' + score + ')').filter(Boolean));
if (params.openInconsistent) openInconsistent(entity, allMusicId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'releases');
beep.play();
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (releases)');
}
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${dataSize} release(s)`, 'gold') });
return mbids[hiIndex];
}),
}];
if (entity == 'artist') lookupMethods.push({ worker: getArtistTracks, resolver: tracks => Promise.all([
amLoadSection('songs', 'div.singleSongResult', function(song) {
const title = song.querySelector('span.songTitle > a');
if (title != null) return {
id: allMusicIdExtractor(resolveUrl(title), 'song'),
title: textAdapter(title),
featArtists: Array.from(song.querySelectorAll('span.songTitle span.featuredArtists > a'), artistAdapter),
composers: Array.from(song.querySelectorAll('span.songComposers > a'), artistAdapter),
type: title.pathname.startsWith('/song/') ? 'song' : 'unknown',
relationType: 'track_artist',
url: resolveUrl(title),
};
}),
amLoadSection('compositions', 'table.compositionsTable > tbody > tr.singleComposition', function(composition) {
const elems = ['td.year', 'td.title a:first-of-type'].map(selector => album.querySelector(selector));
if (elems[1] != null) return {
id: allMusicIdExtractor(resolveUrl(elems[1]), 'composition'),
year: intAdapter(elems[0]),
title: textAdapter(elems[1]),
type: elems[1].pathname.startsWith('/composition/') ? 'composition' : 'unknown',
relationType: 'composer',
url: resolveUrl(elems[1]),
};
}),
]).then(tracks => Array.prototype.concat.apply([ ], tracks.filter(Boolean))).then(function(amTracks) {
function openUncertain() {
GM_openInTab([amOrigin, amEntity(entity), mbids[hiIndex], 'songs'].join('/'), true);
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/'), true);
}
if (amTracks.length <= 0) return Promise.reject('No matches by common tracks');
const mutualScores = tracks.map(tracks => [amTracks, tracks].every(Array.isArray) ? tracks.reduce(function(score, track) {
console.assert(track);
const tracks = amTracks.filter(function(amTrack) {
if ((amTrack.type == 'composition') != ['composer', 'writer'].includes(track.relationType)) return false;
return sameTitleMapper(track, amTrack.title, sameStringValues, trackTitleNorm);
});
if (tracks.length <= 0) return score;
if (debugLogging) console.debug('Found matching tracks:', track, tracks);
//return score + 0.5 + (songs.length - 1) * 0.25;
const base = 1/3, q = trackTitleNorm(track.title).length / 25;
return score + base + q * (1 - base);
}, 0) : 0);
const hiScore = Math.max(...mutualScores);
if (debugLogging) console.debug('Common titles lookup method #2: Entity:', entity,
'AllMusic ID:', allMusicId, 'MBIDs:', mbids, 'Scores:', mutualScores, 'HiScore:', hiScore);
if (!(hiScore > 0)) return Promise.reject('No matches by common tracks');
const hiIndex = mutualScores.indexOf(hiScore);
console.assert(hiIndex >= 0, hiScore, mutualScores);
const dataSize = Math.min(amTracks.length, tracks[hiIndex].length);
if (!(Math.pow(hiScore, 3) * 10 >= Math.min(dataSize, 10) && hiScore * 50 >= dataSize))
if (params.assignUncertain) openUncertain();
else return Promise.reject('Matched by common tracks with too low score');
else if (hiScore < 1) openUncertain();
console.log('Entity binding found by having score %f:\n%s\n%s',
hiScore, [amOrigin, amEntity(entity), allMusicId].join('/'),
[mbOrigin, entity, mbids[hiIndex], 'tracks'].join('/'));
if (mutualScores.filter(score => score > 0).length > 1) {
console.log('Matches by more entities:', mutualScores.map((score, index) =>
score > 0 && [mbOrigin, entity, mbids[index], 'tracks'].join('/') + ' (' + score + ')').filter(Boolean));
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (tracks)');
beep.play();
if (params.openInconsistent) openInconsistent(entity, allMusicId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'tracks');
}
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${dataSize} track(s)`, 'gold') });
return mbids[hiIndex];
}) });
return Promise.all(mbids.map(mbid => entity in amBindingsCache
&& Object.values(amBindingsCache[entity]).includes(mbid) ? Promise.resolve(null)
: mbApiRequest(entity + '/' + mbid, { inc: `aliases+url-rels+${entity}-rels` }).then(function(entry) {
const allMusicIds = getAllMusicRels(entry, amEntity(entity));
if (allMusicIds.includes(allMusicId)) return allMusicIds.length < 2 ? entry.id : true;
if (allMusicIds.length > 0) return null;
return true;
}).catch(function(reason) {
console.warn(reason);
return true;
}))).then(function(statuses) {
let lookupMethod = statuses.filter(mbIdExtractor);
if (lookupMethod.length == 1) {
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} found by sharing same relation`, 'cyan') });
return lookupMethod[0];
} else lookupMethod = (methodIndex = 0) => methodIndex < lookupMethods.length ?
(['worker', 'resolver'].every(fn => typeof lookupMethods[methodIndex][fn] == 'function') ?
Promise.all(statuses.map((status, index) => status
&& lookupMethods[methodIndex].worker(mbids[index]).catch(console.warn)))
.then(resolved => lookupMethods[methodIndex].resolver(resolved))
: Promise.reject('Method not implemented'))
.catch(reason => lookupMethod(methodIndex + 1)) : Promise.reject('Not found by common titles');
return lookupMethod();
}).then(function(mbid) {
saveToCache(entity, allMusicId, mbid);
return mbid;
});
}
function findMBID(entity, allMusicId) {
let promise = getCachedMBID(entity, allMusicId).catch(reason => findAllMusicRelatives(entity, allMusicId).then(function(entries) {
console.assert(entries.length == 1, 'Ambiguous %s linkage for AllMusic id', entity, allMusicId, entries);
if (entries.length > 1) return params.searchSize > 0 ?
findMBIDByCommonTitles(entity, allMusicId, entries.map(entry => entry.id))
: Promise.reject('Ambiguity');
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} found by having AllMusic relative set`, 'salmon') });
saveToCache(entity, allMusicId, entries[0].id);
return entries[0].id;
}));
if (params.searchSize > 0) promise = promise.catch(function(reason) {
if (/^(?:Ambiguity)\b/.test(reason)) return Promise.reject(reason);
if (!(entity in lookupIndexes) || !(allMusicId in lookupIndexes[entity]))
return Promise.reject(`Assertion failed: ${entity}/${allMusicId} not in lookup indexes`);
return mbApiRequest(entity, {
query: searchQueryBuilder(entity, allMusicId),
limit: params.searchSize,
}).then(results => findMBIDByCommonTitles(entity, allMusicId, results[(entity + 's').replace(/s(?=s$)/, '')].filter(function(result) {
if (result.score > 90) return true;
const equal = (name, normFn = str => str) => {
const cmp = root => similarStringValues(normFn(root.name), normFn(name));
return cmp(result) || result.aliases && result.aliases.some(cmp);
};
return equal(lookupIndexes[entity][allMusicId].name) || ['label', 'place'].includes(entity)
&& equal(lookupIndexes[entity][allMusicId].name, entity => entity && entity.replace(...rxBareLabel));
}).map(result => result.id)));
});
return promise;
}
function purgeArtists(fromIndex = 0) {
const artistSuffixes = ['mbid', 'name', 'artist.name', 'join_phrase'];
const key = (ndx, sfx) => `artist_credit.names.${ndx}.${sfx}`;
for (let ndx = 0; artistSuffixes.some(sfx => formData.has(key(ndx, sfx))); ++ndx)
artistSuffixes.forEach(sfx => { formData.delete(key(ndx, sfx)) });
}
function namedBy(entity, artist) {
const namedBy = (entity, artist) => new RegExp('\\b' + nameNorm(artist)
.replace(/[^\w\s]/g, '\\$&') + '\\b', 'i').test(nameNorm(entity));
const nb = (entity, artist) => {
if (namedBy(entity, artist.name)) return true;
if (artist.aliases && artist.aliases.some(alias => namedBy(entity, alias.name))) return true;
return false;
};
return nb(entity.name, artist) || entity.aliases && entity.aliases.some(alias => nb(alias.name, artist));
}
if (debugLogging) console.debug('AllMusic release metadata for %s:', allMusicId, release);
if (!params.mbidLookup) params.searchSize = 0;
const relateAtLevel = sourceEntity => sourceEntity && ({
'work': params.workRelations,
'recording': params.recordingRelations,
'release': params.releaseRelations,
'release-group': params.rgRelations,
}[sourceEntity]);
if (['recording', 'work'].some(relateAtLevel)) params.tracklist = true;
const trackMainArtists = track => (track.artists.length > 0 ? track : release).artists;
const allMusicName = (entity, allMusicId) =>
(entity in lookupIndexes && allMusicId in lookupIndexes[entity] ?
Promise.resolve(lookupIndexes[entity][allMusicId].name)
: globalXHR([amOrigin, amEntity(entity), allMusicId].join('/')).then(({document}) =>
(document = document.body.querySelector('body div[id$="Header"] > div[id$="Headline"] > h1')) != null ?
document.textContent.trim() : Promise.reject('Entity title not found'))
.catch(reason => entity + '#' + allMusicId)).then(name => '<b>' + name + '</b>');
const nameNorm = name => name && toASCII(name);
const hasType = (...types) => types.some(type => formData.getAll('type').includes(type));
formData.set('name', normSeedTitle(release.title));
const lookupIndexes = { }, literals = { }, credits = { }, workers = [ ], rgLookupWorkers = [ ];
const releaseDate = dateParser(release.date);
if (releaseDate != null) {
function setDate(index, part) {
const key = 'events.0.date.' + part;
if ((index = releaseDate[index]) > 0) formData.set(key, index); else formData.delete(key);
}
setDate(0, 'year'); setDate(1, 'month'); setDate(2, 'day');
}
const relationsIndex = {
'artist': {
'work': { 'composer': 165 },
'recording': {
'vocals': 149, 'vocals (background)': 149, 'guest artist': 156,
'mixing': 143, 'producer': 141, 'executive producer': 141, 'co-producer': 141,
'engineer': 138, 'photography': 123,
'art direction': 137,
},
'release': {
'vocals': 60, 'vocals (background)': 60, 'guest artist': 51,
'art direction': 18, 'mixing': 26, 'producer': 30, 'executive producer': 30, 'co-producer': 30,
'mastering': 42, 'remastering': 42, 'engineer': 28, 'composer': 55, 'photography': 20,
'sleeve notes': 24, 'liner notes': 24, 'design': 928, 'sleeve art': 993, 'cover photo': 993,
'audio supervisor': 25, 'assistant photographer': 25, 'project manager': 25, 'project assistant': 25,
},
'release-group': { },
},
}, relationResolvers = { };
if (/ +\([^\(\)]*\b(?:live|(?:en|ao) (?:vivo|directo?))\b[^\(\)]*\)$/i.test(release.title))
formData.append('type', 'Live');
if (/ +\([^\(\)]*\b(?:soundtrack|score)\b[^\(\)]*\)$/i.test(release.title)
|| release.style && release.style.includes('Soundtrack'))
formData.append('type', 'Soundtrack');
(release.labels && release.labels.length > 0 ? release.labels : [undefined]).forEach(function(label, index) {
const prefix = 'labels.' + index;
if (label && label.name) {
formData.set(prefix + '.name', capitalizeName(label.name));
if (rxNoLabel.test(label.name) || release?.artists.some(artist => namedBy(label, artist)))
formData.set(prefix + '.mbid', mb.spl.noLabel);
else addLookupEntry('label', label, prefix);
}
if (release.catalogNumber) formData.set(prefix + '.catalog_number',
rxNoCatno.test(release.catalogNumber) ? '[none]' : release.catalogNumber);
});
if (!Array.isArray(cdLengths) || cdLengths.length <= 0) cdLengths = false;
const media = params.tracklist ? release.media || release.mainAlbum && release.mainAlbum.media : null;
const mediumFormat = medium => medium.format || release.format || 'CD';
if (params.tracklist) if (media != null) media.forEach(function(medium, mediumIndex) {
formData.set(`mediums.${mediumIndex}.format`, mediumFormat(medium));
if (medium.title) formData.set(`mediums.${mediumIndex}.name`, normSeedTitle(medium.title));
medium.tracks.forEach(function(track, trackIndex) {
const prefix = `mediums.${mediumIndex}.track.${trackIndex}.`;
if (track.trackNum) formData.set(prefix + 'number', track.trackNum);
if (track.title) formData.set(prefix + 'name', normSeedTitle(track.title));
const roots = [release, track.artists.length > 0 ? track : release];
if (!['artists', 'featArtists'].every(propName => roots.every(entry1 => roots.every(entry2 =>
(entry2[propName] || [ ]).every(artist2 => (entry1[propName] || [ ]).some(artist1 => artist1.id == artist2.id))))))
seedArtists(track, prefix);
if (track.duration) formData.set(prefix + 'length', track.duration);
});
}); else console.log('AllMusic release', release.id, 'has no tracklist, track names won\'t be seeded');
if (media) for (let medium of media) for (let track of medium.tracks) {
for (let composer of track.composers) addCredit('artist', 'composer', composer);
frequencyAnalysis(literals, track.title);
}
if (Object.keys(literals).length > 0) guessTextRepresentation(formData, literals);
if (params.languageIdentifier && release.media)
workers.push(languageIdentifier(release.media.map(medium => (medium.tracks || [ ]).map(track =>
track.title.replace(...bracketStripper) + '.').join(' ')).join(' ')).then(function(result) {
/*if (!formData.has('language')) */formData.set('language', result.iso6393);
if (params.extendedMetadata) formData.set('language_en', result.language);
notify(`<b>${result.language}</b> identified as release language`, 'deeppink');
}, reason => { console.warn('Remote language identification failed') }));
if (release.artistCredits) {
for (let role in release.artistCredits.extraArtists)
for (let extraArtist of release.artistCredits.extraArtists[role])
addCredit('artist', role, extraArtist);
if ('dj mix' in release.artistCredits.extraArtists) formData.append('type', 'DJ-mix');
}
const relsBlacklist = ['group member'];
if ('artist' in credits) for (let role in credits.artist) {
if (relsBlacklist.some(r => r.toLowerCase() == role.toLowerCase())) continue;
const levels = findRelationLevels('artist', role);
if (levels.length <= 0 && !(role in relationResolvers))
relationResolvers[role] = instrumentResolver(({
'sax (alto)': 'alto saxophone', 'sax (tenor)': 'tenor saxophone',
}[role]) || role).then(instrumentMapper, reason => [25/*, 129, 162*/].map(linkTypeId => ({ linkTypeId: linkTypeId })));
if (levels.length > 0 ? levels.some(relateAtLevel) : role in relationResolvers)
for (let extraArtist of credits.artist[role]) addLookupEntry('artist', extraArtist, role);
}
purgeArtists(1);
seedArtists(release);
if (debugLogging) {
console.debug('Lookup indexes:', lookupIndexes);
console.debug('Credits table:', credits);
}
let urlRelIndex = -1;
addUrlRef([amOrigin, 'album/release', release.id].join('/'), 755);
if (relateAtLevel('release-group') && release.mainAlbum != null)
addUrlRef([amOrigin, 'album', release.mainAlbum.id].join('/'), 284);
//if (params.composeAnnotation) formData.set('annotation', ...);
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from AllMusic release id ${release.id} (${[amOrigin, 'album/release', release.id].join('/')})`).trimLeft());
if (params.rgLookup && params.mbidLookup && !formData.has('release_group') && release.mainAlbum)
rgLookupWorkers.push(findAllMusicRelatives('release-group', release.mainAlbum.id).then(function(releaseGroups) {
console.assert(releaseGroups.length > 0);
console.assert(releaseGroups.length == 1, 'Ambiguous master %d release referencing:', release.master_id, releaseGroups);
return releaseGroups.length == 1 ? releaseGroups[0] : Promise.reject('Ambiguity');
}).catch(reason => null));
if (params.extendedMetadata) {
let tags = release.genres.concat(release.styles), tagIndex = -1;
if (release.mainAlbum != null) {
Array.prototype.push.apply(tags, release.mainAlbum.genres);
Array.prototype.push.apply(tags, release.mainAlbum.styles);
}
for (let tag of (tags = tags.filter(uniqueValues))) formData.set(`tags.${++tagIndex}`, tag);
}
workers.push(getSessions(torrentId).catch(reason => null).then(function(sessions) {
function recordingsLookup(medium, track, mbidLookupFn) {
if (!track) throw 'Invalid argument'; else if (!media) return Promise.reject('Missing media');
if (!track.title) return Promise.reject('Missing track name');
const mediumIndex = media.indexOf(medium);
const trackIndex = medium && medium.tracks ? medium.tracks.indexOf(track) : -1;
console.assert(mediumIndex >= 0 && trackIndex >= 0);
if (layoutMatch(media) > 2) var trackLength = (function getLengthFromTOC() {
if (!sessions || !['CD', 'CD-R'].includes(mediumFormat(medium))
|| !(mediumIndex >= 0) || !(trackIndex >= 0)) return;
const tocEntries = getTocEntries(sessions[mediumIndex]);
if (tocEntries[trackIndex]) return (tocEntries[trackIndex].endSector + 1 -
tocEntries[trackIndex].startSector) * 1000 / 75;
})();
if (!(trackLength > 0)) trackLength = getTrackLength(track);
if (typeof mbidLookupFn != 'function') mbidLookupFn = undefined;
if (!(trackLength > 0) && !mbidLookupFn) return Promise.reject('Missing track length');
const maxLengthDifference = 5000;
const artists = trackMainArtists(track);
console.assert(artists != null, track);
return artists != null ? mbApiRequest('recording', {
query: (function queryBuilder() {
const OR = (values, mapFn) => Array.isArray(values)
&& (values = values.filter((recording, index, arr) => arr.indexOf(recording) == index)).length > 0
&& typeof mapFn == 'function' ? '(' + values.map(mapFn).join(' OR ') + ')' : '';
const fields = artists.map(function(artist) {
const arid = typeof mbidLookupFn == 'function' && artist.id && mbidLookupFn('artist', artist.id);
return arid ? 'arid:' + arid
: '(artistname:"' + artist.name + '" OR creditname:"' + artist.name + '")';
});
fields.push(OR([track.title, track.title.replace(...bracketStripper)],
title => 'recording:"' + title + '"'));
if (trackLength > 0) fields.push(`(${[
`dur:[${Math.max(Math.round(trackLength) - 5000, 0)} TO ${Math.round(trackLength) + 5000}]`,
'(NOT dur:[* TO *])',
].join(' OR ')})`);
if (!canContainVideo(medium)) fields.push('video:false');
return fields.join(' AND ');
})(),
limit: 100,
}).then(function(recordings) {
function recordingValidator(recording, lengthRequired = false, dateRequired = false) {
if (recording.score < 90 || !canContainVideo(medium) && recording.video || [
'(?:re-?)mix(?:ed)?|RMX',
'live|(?:en|ao) (?:vivo|directo?)',
'clean|censored', 'karaoke', 'instrumental',
].some(function(pattern) {
const rx = new RegExp('\\b(?:' + pattern + ')\\b', 'i');
const remoteFlag = rxBracketStripper(undefined, pattern).test(recording.title)
|| rx.test(recording.disambiguation);
const localFlag = rxBracketStripper(undefined, pattern).test(track.title)
|| rxBracketStripper(undefined, pattern).test(release.title);
return remoteFlag != localFlag;
}) || recording.releases && ['Live', 'Interview', 'Demo'].some(function(secondaryType) {
const releases = recording.releases.filter(release => 'release-group' in release);
if (releases.length <= 0) return false;
const count = releases.filter(release => 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType));
return hasType(secondaryType) ? count <= releases.length / 2 : count >= releases.length / 2;
}) || !Array.isArray(recording['artist-credit']) || !artists.every(function(artist) {
const arid = typeof mbidLookupFn == 'function' && mbidLookupFn('artist', artist.id);
return recording['artist-credit'].some(arid ? artistCredit =>
artistCredit.artist && artistCredit.artist.id == arid
: artistCredit => artistCredit.artist && sameStringValues(artist.name, artistCredit.artist.name)
|| artistCredit.name && sameStringValues(artist.name, artistCredit.name));
}) || dateRequired && !recordingDate(recording)) return false;
if (recording.length > 0 ? trackLength > 0 && deltaMapper(recording) > maxLengthDifference
: lengthRequired || !mbidLookupFn) return false;
return sameTitleMapper(recording, track.title, recordingDate(recording)
&& deltaMapper(recording) < 1000 ? weakMatchMapper : deltaMapper(recording) < 3000 ?
similarStringValues : sameStringValues);
}
const deltaMapper = recording => recording.length > 0 && trackLength > 0 ?
Math.abs(recording.length - trackLength) : NaN;
const weakMatchMapper = (...strings) => sameStringValues(...strings)
|| strings.some(str1 => strings.every(str2 => str2.toLowerCase().startsWith(str1.toLowerCase())))
|| strings.every(str => sameStringValues(...[str, strings[0]].map(str => str.replace(...bracketStripper))))
|| similarStringValues(strings[0], strings[1]);
if (recordings.count <= 0 || (recordings = recordings.recordings.filter(recording =>
recordingValidator(recording, false, false))).length <= 0) return Promise.reject('No matches');
return recordings.sort(function(...recordings) {
const hasLength = recording => recording.length > 0;
const cmpVal = fn => fn(recordings[0]) && !fn(recordings[1]) ? -1
: fn(recordings[1]) && !fn(recordings[0]) ? +1 : 0;
return [
function() {
if (!recordings.every(hasLength)) return;
const deltas = recordings.map(deltaMapper);
return deltas[0] < 1000 && deltas[1] >= 1000 || deltas[1] < 1000 && deltas[0] >= 1000
|| Math.abs(deltas[0] - deltas[1]) >= 1000 ? Math.sign(deltas[0] - deltas[1]) : 0;
}, () => recordings.every(recordingDate) ?
recordingDate(recordings[0]).localeCompare(recordingDate(recordings[1])) : 0,
() => cmpVal(recording => sameTitleMapper(recording, track.title)), function() {
if (!recordings.every(hasLength)) return;
const deltas = recordings.map(deltaMapper);
return Math.sign(deltas[0] - deltas[1]);
}, function() {
if (!recordings.every(recording => Array.isArray(recording.releases))) return;
const releases = recordings.map(recording => recording.releases.length);
return Math.sign(releases[1] - releases[0]);
}, () => cmpVal(recordingDate), () => cmpVal(hasLength),
].reduce((result, cmpFn) => result || cmpFn(...recordings), undefined) || 0;
});
}) : Promise.reject('No artists for track');
}
const recordingDate = recording => recording['first-release-date'] || recording.date;
const canContainVideo = medium => medium && medium.format
&& ['BLU-RAY', 'DVD'].includes(medium.format.toUpperCase());
const artistLookupWorkers = { };
if (params.lookupArtistsByRecording && !hasType('Live') && media && params.searchSize > 0)
for (let medium of media) if ('tracks' in medium) for (let track of medium.tracks) (function addArtistLookups(artists) {
if (artists != null) for (let artist of artists) {
if (!(artist.id in artistLookupWorkers)) artistLookupWorkers[artist.id] = [ ];
artistLookupWorkers[artist.id].push(recordingsLookup(medium, track).then(function(recordings) {
const mbids = [ ];
for (let recording of recordings) if (recording.score >= 90 && 'artist-credit' in recording)
for (let artistCredit of recording['artist-credit'])
if (artistCredit.artist && (sameStringValues(artist.name, artistCredit.artist.name)
|| artistCredit.name && sameStringValues(artist.name, artistCredit.name)))
mbids.push(artistCredit.artist.id);
return mbids.length > 0 ? mbids : null;
}).catch(reason => null));
}
})(trackMainArtists(track));
if (params.searchSize > 0) for (let allMusicId in artistLookupWorkers)
artistLookupWorkers[allMusicId] = Promise.all(artistLookupWorkers[allMusicId]).then(function(mbids) {
const scores = { };
for (let _mbids of mbids.filter(Boolean)) for (let mbid of _mbids)
if (!(mbid in scores)) scores[mbid] = 1; else ++scores[mbid];
return Object.keys(scores).length > 0 ? scores : Promise.reject('No matches');
});
return Promise.all(Object.keys(lookupIndexes).map(entity =>
Promise.all(Object.keys(lookupIndexes[entity]).map(function(allMusicId) {
function checkMBID(mbid) {
console.assert(rxMBID.test(mbid), mbid);
if (!rxMBID.test(mbid)) return Promise.reject('Invalid MBID');
if (entity == 'artist' && allMusicId in artistLookupWorkers) artistLookupWorkers[allMusicId].then(function(mbids) {
if (Object.keys(mbids).length > 1)
console.warn('MBID for artist', [amOrigin, 'artist', allMusicId].join('/'),
'can resolve to multiple entities:', printArtistMBIDs(mbids));
if (!Object.keys(mbids).includes(mbid))
console.warn('MBID for artist', [allMusicId, 'artist', allMusicId].join('/'),
'matching different entities:', printArtistMBIDs(mbids));
if (Object.keys(mbids).length > 1 || !Object.keys(mbids).includes(mbid)) {
beep.play();
if (params.openInconsistent) openInconsistent(entity, allMusicId, Object.keys(mbids), 'recordings');
}
});
return mbid;
}
const printArtistMBIDs = mbids => Object.keys(mbids).map(mbid =>
[mbOrigin, 'artist', mbid, 'recordings'].join('/') + ' => ' + mbids[mbid]);
let promise = findMBID(entity, allMusicId);
if (entity == 'artist' && allMusicId in artistLookupWorkers) promise = promise.catch(() =>
artistLookupWorkers[allMusicId].then(function(mbids) {
const hiValue = Math.max(...Object.values(mbids));
if (Object.values(mbids).reduce((sum, count) => sum + count, 0) >= hiValue * 1.5) {
console.warn('MBID for artist', [amOrigin, 'artist', allMusicId].join('/'),
'resolved to multiple entities:', printArtistMBIDs(mbids), '(rejected)');
return Promise.reject('Ambiguity (recordings)');
} else if (Object.keys(mbids).length > 1) {
console.warn('MBID for artist', [amOrigin, 'artist', allMusicId].join('/'),
'resolved to multiple entities:', printArtistMBIDs(mbids), '(accepted)');
beep.play();
if (params.openInconsistent)
openInconsistent(entity, allMusicId, Object.keys(mbids), 'recordings');
}
const mbid = Object.keys(mbids).find(key => mbids[key] == hiValue);
if (!mbid) return Promise.reject('Assertion failed: MBID indexed lookup failed');
if (debugLogging) console.debug('Entity binding found by matching existing recordings:',
[amOrigin, amEntity(entity), allMusicId].join('/') + '#' + entity,
[mbOrigin, entity, mbid, 'releases'].join('/'));
notify(`MBID for ${entity} <b>${lookupIndexes[entity][allMusicId].name}</b> found by match with <b>${hiValue}</b> existing recordings`, 'hotpink');
saveToCache(entity, allMusicId, mbid);
return mbid;
}));
if (entity == 'artist') promise = promise.catch(reason => guessSPA(lookupIndexes[entity][allMusicId].name));
return promise.then(checkMBID).catch(reason => null);
})))).then(function(lookupResults) {
function getMBID(entity, allMusicId) {
console.assert(entity in lookupIndexes);
if (!(entity in lookupIndexes)) return undefined;
let index = Object.keys(lookupIndexes[entity]).findIndex(key => key == allMusicId);
return index >= 0 ? lookupResults[Object.keys(lookupIndexes).indexOf(entity)][index] : undefined;
}
let relIndex = -1, relationWorkers = [ ];
Object.keys(lookupIndexes).forEach(function(entity, ndx1) {
Object.keys(lookupIndexes[entity]).forEach(function(allMusicId, ndx2) {
function addRelation(linkTypeId, attributes, { backward = false, extraData } = { }) {
console.assert(linkTypeId > 0);
const prefix = 'rel.' + ++relIndex;
formData.set(prefix + '.entity', entity);
formData.set(prefix + '.link_type_id', linkTypeId);
if (backward) formData.set(prefix + '.backward', 1);
formData.set(prefix + '.target', mbid);
formData.set(prefix + '.name', lookupIndexes[entity][allMusicId].name);
if (attributes && (attributes = attributes.filter(attribute => attribute.id)).length > 0)
formData.set(prefix + '.attributes', JSON.stringify(attributes));
if (extraData) for (let key in extraData) formData.set(prefix + '.' + key, extraData[key]);
}
const mbid = lookupResults[ndx1][ndx2];
if (mbid != null) for (let context of lookupIndexes[entity][allMusicId].contexts) if (!relsBlacklist.includes(context)) switch(entity) {
case 'artist':
if (entity in credits && context in credits[entity]) {
const levels = findRelationLevels(entity, context);
const resolver = levels.length > 0 ? Promise.resolve(levels.map(level =>
({ linkTypeId: relationsIndex[entity][level][context] }))) : relationResolvers[context];
console.assert(resolver instanceof Promise, entity, context);
if (resolver instanceof Promise) relationWorkers.push(resolver.then(function(relations) {
function isCredited(track) {
function hasEA(root) {
const etraArtists = resolveExtraArtists(track, role => role == context && !levels.some(isReleaseLevel));
return etraArtists && etraArtists.some(extraArtist =>
extraArtist.id == allMusicId && extraArtist.roles.includes(context));
}
return levels.some(isTrackLevel) && (track.trackArtists && track.trackArtists.length > 0 ?
track.trackArtists.some(hasEA) : hasEA(track));
}
const levels = relations.map(relation => findRelationLevel(relation.linkTypeId));
const relateAtTrackLevel = levels.some(isTrackLevel) && Boolean(media)
&& media.some(medium => medium.tracks && medium.tracks.some(isCredited));
console.assert(relations.length > 0);
for (let { linkTypeId, attributes, creditType = context } of relations) {
console.assert(linkTypeId > 0, relations);
if (!(linkTypeId > 0)) continue; else if (!attributes) attributes = [ ];
const sourceLevel = findRelationLevel(linkTypeId);
if (!relateAtLevel(sourceLevel) || isTrackLevel(sourceLevel) != relateAtTrackLevel) continue;
if (linkTypeId == 60) switch (creditType) {
case 'vocals': attributes.push({ id: 'd92884b7-ee0c-46d5-96f3-918196ba8c5b' }); break;
case 'vocals (background)': attributes.push({ id: '75052401-7340-4e5b-a71d-ea024a128849' }); break;
}
if (linkTypeId == 25 || [20, 30, 62, 138, 141, 143, 993].includes(linkTypeId) && [
// Production
// Artwork
'sleeve art', 'cover photo',
].includes(creditType)) attributes.push(taskAttribute(creditType));
if (linkTypeId == 30) switch (creditType) {
case 'executive producer': attributes.push({ id: 'e0039285-6667-4f94-80d6-aa6520c6d359' }); break;
case 'co-producer': attributes.push({ id: 'ac6f6b4c-a4ec-4483-a04e-9f425a914573' }); break;
}
if (linkTypeId == 51) switch (creditType) {
case 'guest artist': attributes.push({ id: 'b3045913-62ac-433e-9211-ac683cdf6b5c' }); break;
}
if (linkTypeId == 42 && ['remastering'].includes(creditType))
attributes.push({ id: '9b72452f-550e-4ace-93ed-fb8789cdc245' });
const extraArtist = credits[entity][context]
.find(extraArtist => extraArtist.id == allMusicId);
console.assert(extraArtist, credits[entity], context, allMusicId);
if (relateAtTrackLevel) media.forEach(function(medium, mediumIndex) {
medium.tracks.forEach(function(track, trackIndex) {
if (isCredited(track)) addRelation(linkTypeId, attributes, {
extraData: { medium: mediumIndex, track: trackIndex },
});
});
}); else if (context in release.artistCredits.extraArtists)
addRelation(linkTypeId, attributes);
}
}).catch(console.log));
console.info('MBID for %s %s:', context, lookupIndexes[entity][allMusicId].name, mbid);
} else formData.set(context + '.mbid', mbid);
break;
default: console.warn('Unexpected entity type:', entity);
}
});
});
if (params.rgLookup && !formData.has('release_group') && Array.isArray(release.artists)) {
function rgResolver(releaseGroups) {
if (!releaseGroups) return null;
const rgFilter = (releaseGroups, strictType = false, strictName = true) => releaseGroups.filter(function(releaseGroup) {
if (formData.has('type') && releaseGroup['primary-type']) {
const types = formData.getAll('type');
const cmpNocase = (...str) => str.every((s, n, a) => s.toLowerCase() == a[0].toLowerCase());
if (!types.some(type => cmpNocase(type, releaseGroup['primary-type']))) return false;
if (strictType && releaseGroup['secondary-types']) {
if (!releaseGroup['secondary-types'].every(secondaryType =>
types.some(type => cmpNocase(type, secondaryType)))) return false;
if (!types.every(type => cmpNocase(type, releaseGroup['primary-type'])
|| releaseGroup['secondary-types'].some(secondaryType =>
cmpNocase(secondaryType, type)))) return false;
}
}
return sameTitleMapper(releaseGroup, release.title, strictName ?
sameStringValues : similarStringValues, releaseTitleNorm)
|| releaseGroup.releases && releaseGroup.releases.some(release2 =>
sameTitleMapper(release2, release.title, strictName ?
sameStringValues : similarStringValues, releaseTitleNorm));
});
let filtered = rgFilter(releaseGroups, false, true);
if (filtered.length > 1) filtered = rgFilter(releaseGroups, true, true);
else if (filtered.length < 1) filtered = rgFilter(releaseGroups, false, false);
if (filtered.length != 1) filtered = rgFilter(releaseGroups, true, false);
return filtered.length == 1 ? filtered[0] : null;
}
Array.prototype.push.apply(rgLookupWorkers, release.artists.map(function(artist) {
if (artist.id == 194) return;
const mbid = getMBID('artist', artist.id);
if (mbid) return mbLookupById('release-group', 'artist', mbid).then(rgResolver, console.error);
}).filter(Boolean));
if (release.artists.length > 0) {
const normTitle = releaseTitleNorm(release.title);
rgLookupWorkers.push(mbApiRequest('release-group', { query: ['(' + [
`releasegroup:"${release.title}"`, `releasegroup:"${normTitle}"`,
`releasegroup:"${normTitle.replace(/(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]))+$/g, '')}"`,
`alias:"${release.title}"`, `alias:"${normTitle}"`,
`release:"${release.title}"`, `release:"${normTitle}"`,
].join(' OR ') + ')'].concat(release.artists.map(function(artist) {
const arid = getMBID('artist', artist.id);
return arid ? 'arid:' + arid
: '(artistname:"' + artist.name + '" OR creditname:"' + artist.name + '")';
})).join(' AND '), limit: 100 }).then(results => rgResolver(results['release-groups']), console.error));
}
}
return Promise.all(rgLookupWorkers).then(function releaseGroupResolver(releaseGroups) {
const releaseGroup = releaseGroups.find(Boolean);
if (releaseGroup) formData.set('release_group', releaseGroup.id); else return false;
let notification = `MBID for release group <b>${releaseGroup.name || releaseGroup.title}</b>`;
if (releaseGroup['first-release-date'])
notification += ` (<b>${getReleaseYear(releaseGroup['first-release-date'])}</b>)`;
notification += ` found by ${'relations' in releaseGroup ? 'unique name match' : 'known URL relation'}`;
notify(notification, 'goldenrod');
return true;
}).then(function findExistingRecordings() {
if (!(params.recordingsLookup > 0) || !media || !(params.recordingsLookup > 1)
&& !hasType('Single') && (formData.has('release_group') || hasType('DJ-mix', 'Remix', 'Live')))
return false;
return Promise.all(media.map(function(medium, mediumIndex) {
if (!medium || !Array.isArray(medium.tracks) || canContainVideo(medium))
return false;
return Promise.all(medium.tracks.map(function(track, trackIndex) {
if (params.recordingsLookup > 1) return recordingsLookup(medium, track, getMBID).then(function(recordings) {
if ((recordings = recordings.filter(recording => !/\b(?:live)\b/i
.test(recording.disambiguation))).length <= 0) return Promise.reject('No matches');
formData.set(`mediums.${mediumIndex}.track.${trackIndex}.recording`, recordings[0].id);
let notifyText = `MBID for recording <b>${track.title}</b> found`, firstRelease = [ ];
if (recordingDate(recordings[0])) firstRelease.push('<b>' +
getReleaseYear(recordingDate(recordings[0])) + '</b>');
if (recordings[0].releases && recordings[0].releases.length > 0) {
const release = recordings[0].releases.length > 1 ? recordings[0].releases.find(release =>
release.date == recordingDate(recordings[0])) : recordings[0].releases[0];
if (release) {
let releaseType = release['release-group'] && release['release-group']['primary-type'];
if (releaseType && releaseType.toUpperCase() != releaseType) releaseType = releaseType.toLowerCase();
if (releaseType && release['release-group']['secondary-types']
&& release['release-group']['secondary-types'].includes('Live'))
releaseType = 'live ' + releaseType;
firstRelease.push('on <b>' + (releaseType ? releaseType + ' ' + release.title : release.title) + '</b>');
}
}
if (firstRelease.length > 0) notifyText += ` (first released ${firstRelease.join(' ')})`;
notify(notifyText, 'orange');
if (debugLogging) console.debug('Closest recordings for track %o:', track, recordings);
}).catch(reason => { /*console.info('No recording for track %o found (%s)', track, reason)*/ });
}));
}));
}).then(() => Promise.all(relationWorkers));
});
}));
return Promise.all(workers).then(() => formData);
});
}
function finalizeSeed(formData) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
// if (!formData.has('language')) formData.set('language', 'eng');
const releaseTypes = formData.getAll('type');
if (formData.get('artist_credit.names.0.mbid') == mb.spa.VA && !releaseTypes.includes('Compilation')
&& !['Soundtrack', 'Live'].some(secondaryType => releaseTypes.includes(secondaryType)))
formData.append('type', 'Compilation');
if (!formData.has('script') && formData.has('language')) {
const script = scriptFromLanguage(formData.get('language'));
if (script) formData.set('script', script);
}
return getSessions(torrentId).catch(reason => null).then(function(sessions) {
if (sessions != null) sessions.forEach(function(session, discIndex) {
const key = `mediums.${discIndex}.format`, format = formData.get(key);
if (!format || format == 'CD') switch (getLayoutType(getTocEntries(session))) {
case 0: formData.set(key, 'CD'); break;
case 1: formData.set(key, 'Enhanced CD'); break;
case 2: formData.set(key, 'Copy Control CD'); break;
default: console.warn(`Disc ${discIndex + 1} unknown TOC type`, getTocEntries(session));
}
});
return formData;
});
}
function seedNewRelease(formData, makeVotable = !(mbSeedNew >= 2)) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
if (scriptSignature) formData.set('edit_note', ((formData.get('edit_note') || '') + '\nSeeded by ' + scriptSignature).trimLeft());
if (makeVotable) formData.set('make_votable', 1);
const form = Object.assign(document.createElement('form'), {
method: 'POST',
action: mbOrigin + '/release/add',
target: '_blank',
hidden: true,
});
for (let entry of formData) form.append(Object.assign(document.createElement(entry[1].includes('\n') ?
'textarea' : 'input'), { name: entry[0], value: entry[1] }));
document.body.appendChild(form).submit();
document.body.removeChild(form);
}
function editNoteFromSession(session) {
let editNote = GM_getValue('insert_upload_reference', false) ?
`Release identification from torrent ${document.location.origin}/torrents.php?torrentid=${torrentId} edition info\n` : '';
editNote += 'TOC derived from EAC/XLD ripping log';
if (session) editNote += '\n\n' + (mbSubmitLog ? session
: 'Medium fingerprint:\n' + getMediaFingerprint(session)) + '\n';
if (scriptSignature) editNote += '\nSubmitted by ' + scriptSignature;
return editNote;
}
const attachToMB = (mbId, attended = false, skipPoll = false) => getMbTOCs().then(function(mbTOCs) {
function attachByHand() {
for (let discNumber = mbTOCs.length; discNumber > 0; --discNumber) {
url.searchParams.setTOC(discNumber - 1);
GM_openInTab(url.href, discNumber > 1);
}
return false;
}
const url = new URL('/cdtoc/attach', mbOrigin);
url.searchParams.setTOC = function(index = 0) { this.set('toc', mbTOCs[index].join(' ')) };
const warnings = [ ];
return (mbId ? rxMBID.test(mbId) ? mbApiRequest('release/' + mbId, { inc: 'media+discids' }).then(function(release) {
if (release.media && sameMedia(release).length < mbTOCs.length)
return Promise.reject('not enough attachable media in this release');
url.searchParams.set('filter-release.query', mbId);
warnings[0] = sameMedia(release).map(medium => medium.discs ? medium.discs.length : 0);
return mbId;
}) : Promise.reject('invalid format') : Promise.reject(false)).catch(function(reason) {
if (reason) alert(`Not linking to release id ${mbId} for the reason ` + reason);
}).then(mbId => mbId && !attended && mbAttachMode > 1 ? Promise.all(mbTOCs.map(function(mbTOC, tocNdx) {
url.searchParams.setTOC(tocNdx);
return globalXHR(url).then(({document}) =>
Array.from(document.body.querySelectorAll('table > tbody > tr input[type="radio"][name="medium"][value]'), input => ({
id: input.value,
title: input.nextSibling && input.nextSibling.textContent.trim().replace(/(?:\r?\n|[\t ])+/g, ' '),
})));
})).then(function(mediums) {
mediums = mediums.every(medium => medium.length == 1) ? mediums.map(medium => medium[0]) : mediums[0];
if (mediums.length != mbTOCs.length)
return Promise.reject('Unable to reliably bind volumes or not logged in');
if (Array.isArray(warnings[0])) if (warnings[0].every(mediumIds => mediumIds > 0))
warnings[0] = `all CD media already have assigned at least ${Math.min(...warnings[0])} disc id(s)`;
else if (warnings[0].some(mediumIds => mediumIds > 0))
warnings[0] = `some CD media already have assigned at least ${Math.min(...warnings[0].filter(mediaIds => mediaIds > 0))} disc id(s)`;
else warnings[0] = false; else warnings[0] = false;
if (logScoreTest(uncalibratedReadOffset))
warnings.push('at least one session ripped with wrong read offset');
if (logScoreTest(logStatus => !(logStatus.score > 0)))
warnings.push('at least one logfile seems to have very bad score');
if (!confirm([
[
`${mbTOCs.length != 1 ? mbTOCs.length.toString() + ' TOCs are' : 'TOC is'} going to be attached to release id ${mbId}`,
], warnings.filter(Boolean).map(warning => 'WARNING: ' + warning), [
mediums.length > 1 && 'Media titles:\n' + mediums.map(medium => '\t' + medium.title).join('\n'),
'Submit mode: ' + (!skipPoll && mbAttachMode < 3 ? 'apply after poll close (one week or sooner)' : 'auto-edit (without poll)'),
'Edit note: ' + (mbSubmitLog ? 'entire .LOG file per volume' : 'medium fingerprint only'),
], [
'Before you confirm make sure -',
'- uploaded CD and MB release are identical edition',
'- attached log(s) have no score deductions for uncalibrated read offset',
],
].map(lines => lines.filter(Boolean).join('\n')).filter(Boolean).join('\n\n'))) return false;
const postData = new FormData;
if (!skipPoll && mbAttachMode < 3) postData.set('confirm.make_votable', 1);
return getSessions(torrentId).then(sessions => Promise.all(mbTOCs.map(function(mbTOC, index) {
url.searchParams.setTOC(index);
url.searchParams.set('medium', mediums[index].id);
postData.set('confirm.edit_note', editNoteFromSession(sessions[index]));
return globalXHR(url, { responseType: null }, postData);
}))).then(responses => (GM_openInTab([mbOrigin, 'release', mbId, 'discids'].join('/'), false), true));
}).catch(reason => (alert(reason + '\n\nAttach by hand'), attachByHand())) : attachByHand());
}, reason => (alert(reason), false));
function attachToMBIcon(mbId, style, tooltip, tooltipster) {
// <svg height="0.9em" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
// <circle fill="${staticIconColor}" cx="50" cy="50" r="50"/>
// <path fill="white" d="M68.13 32.87c2.18,-0.13 5.27,-0.31 7.1,0.94 2.03,1.37 2.1,3.8 1.97,6.01 -0.55,9.72 -0.13,19.52 -0.21,29.25 -0.05,5.73 -2.35,10.96 -6.57,14.84 -4.85,4.46 -11.39,6.42 -17.86,6.78 -0.04,0 -0.08,0.01 -0.11,0.01l-4.5 0c-0.03,0 -0.05,0 -0.07,-0.01 -0.18,0 -0.36,-0.01 -0.54,-0.02 -6.89,-0.43 -14,-2.82 -19,-7.74 -2.53,-2.48 -4.26,-5.23 -5.01,-8.71 -0.62,-2.89 -0.49,-5.88 -0.47,-8.81 0.1,-12.96 -0.24,-25.93 -0.02,-38.89 0.05,-3.01 0.42,-5.94 1.97,-8.57 1.81,-3.07 4.83,-5.05 7.99,-6.53 2.69,-1.26 5.73,-1.91 8.69,-2.11 0.05,0 0.09,-0.01 0.13,-0.01l3.59 0c0.05,0 0.08,0.01 0.13,0.01 3.61,0.23 7.41,1.15 10.55,2.99 3.09,1.8 5.79,4.11 7.13,7.52 1.09,2.79 1.07,5.79 1.01,8.74 -0.14,6.49 -0.06,12.98 -0.02,19.47 0.02,4.95 0.98,15.66 -1.95,19.61 -2.26,3.06 -6.11,4.59 -9.79,5 -3.82,0.43 -8.01,-0.26 -11.32,-2.27 -3.28,-1.98 -4.54,-5.39 -4.89,-9.04 -0.16,-1.6 -0.14,-3.22 -0.07,-4.83 0.05,-1.32 0.15,-2.64 0.16,-3.96 0.05,-6.1 0.06,-12.21 0.21,-18.31 0.03,-1.09 0.92,-1.95 2,-1.95l6.37 0c1.1,0 2,0.9 2,2 0,2.02 -0.09,4.06 -0.14,6.08 -0.08,3.09 -0.14,6.18 -0.15,9.27 0,2.99 0.02,6.03 0.23,9.01 0.06,0.87 0.29,3.78 0.7,4.63 1.08,0.91 3.95,0.88 5.06,0.1 1.09,-0.76 0.71,-3.87 0.68,-4.99 -0.14,-5.16 -0.01,-10.32 -0.01,-15.48 0,-5.21 -0.07,-10.42 0.03,-15.63 0.08,-4.8 -0.58,-7.19 -5.63,-8.72 -2.35,-0.71 -4.97,-0.78 -7.36,-0.21 -1.96,0.47 -4.04,1.46 -5.29,3.08 -1.77,2.29 -1.09,10.23 -1.08,13.15 0.02,10.39 0.1,20.78 0.01,31.16 -0.04,4.6 -0.76,8.12 2.93,11.61 6.55,6.2 19.73,6.26 26.32,0.08 3.76,-3.53 3.06,-6.86 3.02,-11.54 -0.09,-10.33 0.01,-20.67 0.01,-31 0,-0.77 0.4,-1.42 1.08,-1.77 0.32,-0.17 0.66,-0.25 0.99,-0.24z"/>
return addIcon(minifyHTML(`
<svg height="0.9em" fill="#aa0" viewBox="0 0 56 100" xmlns="http://www.w3.org/2000/svg">
<path d="M43.56 30.51c0,0 0.77,-4.33 -4.16,-4.33 -4.93,0 -4.14,4.24 -4.14,4.24l0 37.66c0,0 1.69,10.1 -7.26,10.02 -8.94,-0.1 -7.26,-10.02 -7.26,-10.02l0 -49.37c0,0 -0.77,-12.11 13.49,-12.12 14.25,0.01 13.47,12.15 13.47,12.15l0 55.48c0,0 1.81,19.13 -19.82,19.13 -21.64,0 -19.56,-19.13 -19.56,-19.13l-0.01 -42.13c0,0 0.51,-4.33 -4.15,-4.33 -4.66,0 -4.14,4.33 -4.14,4.33l0 48.33c0,0 1.16,19.13 27.98,19.13 26.83,0 28,-19.13 28,-19.13l-0.01 -66.44c0,0 -0.9,-13.98 -21.76,-13.98 -20.87,0 -21.77,13.98 -21.77,13.98l-0.01 57.16c0,0 -1.8,13.27 15.69,13.27 17.49,0 15.41,-13.27 15.41,-13.27l0.01 -40.63z"/>
</svg>`), function clickHandler(evt) {
let target = evt.currentTarget;
if (target instanceof HTMLElement) target.disabled = true; else target = null;
const animation = target && flashElement(target);
attachToMB(mbId, evt.altKey, evt.ctrlKey).then(function(attached) {
if (animation != null) animation.cancel();
if (target != null) target.disabled = false;
});
}, !mbId && function dropHandler(evt, urls) {
const mbReleaseId = urls.map(url => mbIdExtractor(url, 'release')).find(Boolean);
if (!mbReleaseId) return;
let target = evt.currentTarget;
if (target instanceof HTMLElement) target.disabled = true; else target = null;
const animation = target && flashElement(target);
attachToMB(mbReleaseId, evt.altKey, evt.ctrlKey).then(function(attached) {
if (animation != null) animation.cancel();
if (target != null) target.disabled = false;
});
}, 'attach-toc', style, tooltip, tooltipster);
}
function seedToMB(target, torrent, params) {
function seedToMb() {
if (logScoreTest(uncalibratedReadOffset)
&& !confirm('At least one session ripped with wrong read offset, continue anyway?')
|| logScoreTest(logStatus => !(logStatus.score > 0))
&& !confirm('At least one logfile seems to have very bad score, continue anyway?')) return;
return getMbTOCs().then(function(mbTOCs) {
const formData = new URLSearchParams;
if (rxMBID.test(params.releaseGroupId)) formData.set('release_group', params.releaseGroupId);
seedFromTorrent(formData, torrent, params && params.torrentReference);
return seedFromTOCs(formData, mbTOCs)
.then(formData => params.allMusicId ?
seedFromAllMusic(formData, params.allMusicId, params, mbTOCs.map(mbTOC => mbTOC[1])) : formData)
.then(formData => params.discogsId > 0 ?
seedFromDiscogs(formData, params.discogsId, params, mbTOCs.map(mbTOC => mbTOC[1])) : formData);
}).then(finalizeSeed).then(formData => seedNewRelease(formData, params && params.makeVotable));
}
function relationsHandler(releases) {
console.assert(releases.length > 0);
if (releases.length > 1) return confirm(`This release already exists by ambiguous binding from
${releases.map(release => `\t${mbOrigin}/release/${release.id}`).join('\n')}
Create new release anyway?`) ?
Promise.reject('New release enforced') : Promise.resolve('Cancelled');
return confirm(`This release already exists as ${mbOrigin}/release/${releases[0].id}.
Attach the TOC(s) instead?`) ? attachToMB(releases[0].id, false, false) : Promise.reject('New release enforced');
}
if (!(target instanceof HTMLElement) || !torrent) throw 'Invalid argument';
if (!params) params = { };
return (params.discogsId > 0 ? findDiscogsRelatives('release', params.discogsId).then(relationsHandler) : Promise.reject('No Discogs relations'))
.catch(() => params.allMusicId ? findAllMusicRelatives('album/release', params.allMusicId, 'release').then(relationsHandler) : Promise.reject('No AllMusic relations'))
.catch(seedToMb);
}
function updateFromExternalDb(mbid, metaCollector, sourceRef, params) {
if (!mbid || typeof metaCollector != 'function') throw 'Invalid argument';
params = Object.assign({
overwrite: false,
makeVotable: !(mbUpdateRelease >= 2),
createMissingWorks: GM_getValue('mb_create_works', 1),
simulationMode: false,
}, params);
if (params.simulationMode) params.createMissingWorks = 0;
return Promise.all([
globalXHR([mbOrigin, 'release', mbid, 'edit'].join('/')).then(function({document}) {
const objects = { sourceEntity: (function(scripts) {
for (let script of scripts) {
let obj = /^Object\.defineProperty\(window,\s*"__MB__",\s*(.+)\)$/.exec(script.text?.trim());
if (obj != null) try { if (obj = eval(obj[1])['$c'].stash.source_entity) return obj }
catch(e) { console.warn(e) }
}
})(document.getElementsByTagName('script')) };
document.body.querySelectorAll('div#release-editor select[id]').forEach(function(select) {
objects[select.id.toLowerCase() + 'Ids'] = Object.assign.apply({ },
Array.from(select.options, option => ({ [option.text]: parseInt(option.value) }))
.filter(elem => Object.values(elem).every(value => !isNaN(value))));
});
return objects;
}, console.warn),
mbApiRequest('release/' + mbid, { inc: [
'release-groups', 'artist-credits', 'labels', 'media', 'recordings', 'annotation', 'tags',
'artist-rels', 'label-rels', 'series-rels', 'place-rels', 'work-rels', 'url-rels',
'release-group-level-rels', 'recording-level-rels', 'work-level-rels',
].join('+') }),
getMbTOCs(),
]).then(([mbEditObjects, mbRelease, mbTOCs]) => metaCollector({
labguageIdentifier: params.overwrite || !mbRelease['text-representation'].language,
}, mbTOCs.map(mbTOC => mbTOC[1])).then(finalizeSeed).then(function(formData) {
function findEditId(indexName, field) {
if (!indexName || !field || !mbEditObjects || !mbEditObjects[indexName = indexName + 'Ids'])
return null;
const index = Object.keys(mbEditObjects[indexName]).find(key => sameStringValues(key, field));
return index ? mbEditObjects[indexName][index] : null;
}
function objectsEqual(a, b) {
function cmp(a, b) {
for (let prop in a) if (a.hasOwnProperty(prop) && (!b.hasOwnProperty(prop)
|| (typeof a[prop] == 'object' ? !objectsEqual(a[prop], b[prop]) : a[prop] !== b[prop])))
return false;
return true;
}
return [a, b].every(o => o == null) || [a, b].every(Boolean) && cmp(a, b) && cmp(b, a);
}
if (debugLogging) console.debug('Edit objects:', mbEditObjects, mbRelease);
if (mbRelease.barcode && formData.get('barcode')
&& !sameBarcodes(mbRelease.barcode, formData.get('barcode'))
&& !confirm(`Releases don't match by barcode: ${mbRelease.barcode} ≠ ${formData.get('barcode')},\nApply edits anyway?`))
return;
const edits = [ ], batchEdits = { 32: { } };
let events = [ ], updateEvents = params.overwrite, tags = [ ];
if (params.updateMetadata && (params.overwrite || !mbRelease.status)) {
let statusId = formData.get('status');
if (statusId = findEditId('status', statusId) || statusId)
batchEdits[32].status_id = statusId;
}
if (params.updateMetadata && (params.overwrite || !mbRelease.packaging)) {
let packagingId = formData.get('packaging');
if (packagingId = findEditId('packaging', packagingId) || packagingId)
batchEdits[32].packaging_id = packagingId;
}
if (params.updateMetadata && (params.overwrite || !mbRelease['text-representation'].language)) {
let languageId = formData.get('language_en');
if (languageId && (languageId = findEditId('language', languageId))
|| (languageId = formData.get('language')) && (languageId = findEditId('language', ({
eng: 'English', deu: 'German', spa: 'Spanish', fra: 'French', ita: 'Italian',
zho: 'Chinese', rus: 'Russian', jpn: 'Japanese', kor: 'Korean', tha: 'Thai',
swe: 'Swedish', nor: 'Norwegian', fin: 'Finnish', por: 'Portugese', nld: 'Dutch',
pol: 'Polish', ces: 'Czech', slk: 'Slovak', slv: 'Slovenian', hrv: 'Croatian',
srp: 'Serbian', hun: 'Hungarian', tur: 'Turkish', dan: 'Danish', ltz: 'Lithuanian',
ron: 'Romanian', est: 'Estonian', lav: 'Latvian', isl: 'Islandian', lat: 'latin',
cat: 'Catalanian', hin: 'Hindi', ell: 'Greek', vie: 'Vietnamese', heb: 'Hebrew',
bul: 'Bulgarian', mak: 'Macedonian', ara: 'Arabic', bos: 'Bosnian', afr: 'Afrikaans',
// 'gsw', 'fil', 'eus', 'lit', 'cym', 'glg', 'bre', 'oci', 'haw', 'gla',
// 'zul', 'ast', 'swa', 'som', 'gle', 'ukr', 'bel', 'mon', 'mal', 'asm', 'kat',
// 'mar', 'san', 'ind', 'fas', 'urd', 'msa', 'kaz', 'tuk', 'uzb', 'nob', 'mri',
// 'yid', 'grk', 'gre', 'hye', 'pan', 'tam', 'tel', 'mya',
}[languageId])))) batchEdits[32].language_id = languageId;
}
if (params.updateMetadata && (params.overwrite || !mbRelease['text-representation'].script)) {
let scriptId = formData.get('script');
if (scriptId = findEditId('script', ({
Latn: 'Latin', Cyrl: 'Cyrillic', Jpan: 'Japanese', Kore: 'Korean', Thai: 'Thai', Arab: 'Arabic',
Grek: 'Greek', Hebr: 'Hebrew', Hant: '', Deva: '', Armn: '', Guru: '', Taml: 'Tamil',
Hani: '', Telu: '', Mymr: '', Mlym: '', Beng: '', Geor: '',
}[scriptId])) || scriptId) batchEdits[32].script_id = scriptId;
}
if (params.updateMetadata && (params.overwrite || !mbRelease.barcode && formData.get('barcode')))
batchEdits[32].barcode = formData.get('barcode');
const isAreaCode = (area, countryCode) => area && countryCode
&& area['iso-3166-1-codes'].some(isoCode => isoCode.toUpperCase() == countryCode.toUpperCase());
const countryIdFromEvent = event => event.area ? mbEditObjects && mbEditObjects.sourceEntity
&& mbEditObjects.sourceEntity.events[mbRelease['release-events'].indexOf(event)].country.id
|| event.area['iso-3166-1-codes'] : null;
if (params.updateMetadata) for (let index = 0; ['country', 'date.year'].some(suffix => formData.has(`events.${index}.${suffix}`)); ++index) {
let country = formData.get(`events.${index}.country`);
let date = ['year', 'month', 'day'].map(unit => (unit = formData.get(`events.${index}.date.${unit}`))
&& !isNaN(unit = parseInt(unit)) ? unit : undefined);
const event = mbRelease['release-events'] && (mbRelease['release-events'].find(event => isAreaCode(event.area, country))
|| mbRelease['release-events'].find(event => (!country || !event.area)
&& (date[0] > 0 && (event = dateParser(event.date)) != null && event[0] == date[0])));
if (country) country = [country];
if (event) {
if (event.area) country = countryIdFromEvent(event); else if (country) updateEvents = true;
const eventDate = dateParser(event.date);
if (eventDate != null) {
if (eventDate.some((unit, index) => unit > 0 && (!(date[index] > 0))
|| !params.overwrite && date[index] != unit)) date = eventDate;
if (date.some((unit, index) => unit > 0 && !(eventDate[index] > 0))) updateEvents = true;
} else if (date[0] > 0) updateEvents = true;
} else updateEvents = true;
events.push({
country_id: country || null,
date: { year: date[0] || null, month: date[1] || null, day: date[2] || null },
});
}
if (params.updateMetadata && updateEvents) {
if (!params.overwrite && mbRelease['release-events']) Array.prototype.unshift.apply(events,
mbRelease['release-events'].map(function(releaseEvent, eventIndex) {
const date = dateParser(releaseEvent.date);
const event = events.some(event => (date == null || event.date != null && event.date.year == date[0])
&& (!releaseEvent.area || event.country_id && (Array.isArray(event.country_id) ?
event.country_id.some(countryId => isAreaCode(releaseEvent.area, countryId))
: event.country_id == mbEditObjects.sourceEntity.events[eventIndex].country.id)));
return !event && {
country_id: countryIdFromEvent(releaseEvent),
date: date && { year: date[0] || null, month: date[1] || null, day: date[2] || null },
};
}).filter(Boolean));
events = events.map(event => Array.isArray(event.country_id) ? Promise.all(event.country_id.map(country =>
mbApiRequest('area', { query: 'iso1:' + country }).then(function(results) {
let countryId = results.areas.filter(area =>
(area.type == 'Country' || !area.type) && isAreaCode(area, country));
if (countryId.length == 1) countryId = countryId[0];
else return Promise.reject('Country/reegion id for ' + country + ' not found');
return findEditId('country', countryId.name)/* || countryId.id*/;
}).catch(console.warn))).then(results => results.find(Boolean))
.then(countryId => Object.assign(event, { country_id: countryId || null })) : event);
}
if (params.updateMetadata && !mbRelease.disambiguation && formData.get('comment'))
batchEdits[32].comment = formData.get('comment');
if (params.updateMetadata && (Object.keys(batchEdits[32]).length > 0 || updateEvents))
edits.push(Promise.all(events).then(function(events) {
if (updateEvents && (events = events.filter(Boolean)).length > 0) batchEdits[32].events = events;
return Object.keys(batchEdits[32]).length > 0 ? Object.assign({
edit_type: 32,
to_edit: mbRelease.id,
}, batchEdits[32]) : null;
}));
const matchedLabels = new Set;
for (let index = 0; ['name', 'catalog_number'].some(suffix => formData.has(`labels.${index}.${suffix}`)); ++index) {
const mbid = formData.get(`labels.${index}.mbid`);
const name = formData.get(`labels.${index}.name`);
if (name && !mbid) continue;
const catNo = formData.get(`labels.${index}.catalog_number`);
const labelInfo = mbRelease['label-info'] || [ ];
const labelMatch = labelInfo => mbid && labelInfo.label && labelInfo.label.id == mbid;
const catNoMatch = labelInfo => catNo && sameStringValues(labelInfo['catalog-number'], catNo);
if (labelInfo.some(labelInfo => labelMatch(labelInfo) && catNoMatch(labelInfo))) continue;
const label = labelInfo.find((labelInfo, index) => (!labelInfo.label || !mbid || labelMatch(labelInfo))
&& (!labelInfo['catalog-number'] || !catNo || catNoMatch(labelInfo)) && !matchedLabels.has(index));
edits.push(Object.assign(!label || !mbEditObjects || !mbEditObjects.sourceEntity ? {
edit_type: 34,
release: mbRelease.id,
} : {
edit_type: 37,
release_label: mbEditObjects.sourceEntity.labels[labelInfo.indexOf(label)].id,
}, { label: mbid || null, catalog_number: catNo || null }));
if (label) matchedLabels.add(labelInfo.indexOf(label));
}
if (params.updateMetadata && !mbRelease.annotation && formData.get('annotation')) edits.push({
edit_type: 35,
entity: mbRelease.id,
text: formData.get('annotation'),
});
const workWorkers = new Map, assignedWorks = new Set, reusedWorks = new Map;
for (let index = 0; formData.has(`rel.${index}.entity`); ++index) {
function createEdit(entity) {
const entities = [{ entityType: targetEntity, gid: target/*, name: name*/ }];
entities[backward ? 'unshift' : 'push'](entity);
const edit = { edit_type: 90, linkTypeID: linkTypeId, entities: entities };
edit.attributes = attributes && attributes.length > 0 ? attributes.map(function(attribute) {
const _attribute = { type: { gid: attribute.id } };
if (attribute.creditedAs) _attribute.credited_as = attribute.creditedAs;
if (attribute.value) _attribute.text_value = attribute.value;
return _attribute;
}) : null;
if (creditedAs) if (![name, capitalizeName(name)].includes(creditedAs))
edit[`entity${backward ? 1 : 0}_credit`] = creditedAs;
// else return mbApiRequest(targetEntity + '/' + target).then(function(entity) { // strict credit name
// if (![entity.name, capitalizeName(entity.name)].includes(creditedAs))
// edit[`entity${backward ? 1 : 0}_credit`] = creditedAs;
// }, console.warn).then(() => edit);
return edit;
}
const targetEntity = formData.get(`rel.${index}.entity`);
const target = formData.get(`rel.${index}.target`);
const linkTypeId = parseInt(formData.get(`rel.${index}.link_type_id`));
const backward = Boolean(parseInt(formData.get(`rel.${index}.backward`)));
console.assert(targetEntity && target && linkTypeId > 0);
if (!targetEntity || !target || !(linkTypeId > 0)) continue;
const relationLevel = findRelationLevel(linkTypeId);
console.assert(relationLevel, 'No relation level for unknown link type id', linkTypeId, targetEntity);
if (!relationLevel) continue; else console.assert(mbRelationsIndex[relationLevel][linkTypeId], relationLevel, linkTypeId);
const name = formData.get(`rel.${index}.name`), creditedAs = formData.get(`rel.${index}.credit`);
let attributes = formData.get(`rel.${index}.attributes`);
if (attributes) try { attributes = JSON.parse(attributes) } catch(e) { console.warn(e) }
const getWorks = track => (((track || { }).recording || { }).relations || [ ])
.filter(relation => relation['target-type'] == 'work' && relation.type == 'performance')
.map(relation => relation.work);
const hasRelation = root => root && root.relations && root.relations.some(function(relation) {
if (!relation[targetEntity] || !relation[targetEntity].id) return false;
const hasType = (...linkTypeIds) => linkTypeIds.some(function(linkTypeId) {
const relationLevel = findRelationLevel(linkTypeId);
console.assert(relationLevel, 'No relation level for unknown link type id', linkTypeId);
if (!relationLevel) return false; // assertion failed!
return relation.type == mbRelationsIndex[relationLevel][linkTypeId];
});
if ([
target, mb.spa.VA, mb.spa.noArtist, /*mb.spa.unknown, */mb.spa.anonymous, mb.spa.traditional,
mb.spa.dialogue, mb.spa.data, mb.spa.disney, mb.spa.theatre, mb.spa.churchChimes, mb.spa.languageInstruction,
].includes(relation[targetEntity].id.toLowerCase())) switch (linkTypeId) {
case 54: case 167: return hasType(54, 55, 56, 57, 165, 167, 168, 169);
case 55: case 168: return hasType(54, 55, 167, 168);
case 56: case 165: return hasType(54, 56, 57, 165, 167, 169);
case 57: case 169: return hasType(54, 56, 57, 165, 167, 169);
case 51: case 156: return hasType(44, 51, 60, 148, 149, 156);
case 208: case 362: if (hasType(208, 362)) break; else return false;
default: if (!hasType(linkTypeId)) return false;
} else return false;
return !attributes || attributes.every(function hasAttribute(attribute) {
function hasAttribute(attributeId = attribute.id) {
if (!attributeId) return true;
const index = relation?.attributes.find(attribute =>
relation?.['attribute-ids']?.[attribute].toLowerCase() == attributeId);
if (index == undefined) return false; else if (!attribute.value) return true;
return relation?.['attribute-values']?.[index].toString().toLowerCase()
== attribute.value.toString().toLowerCase();
}
if ([
'0a5341f8-3b1d-4f99-a0c6-26b7f4e42c7f', // additional
'8c4196b1-7053-4b16-921a-f22b2898ed44', // assistant
'8d23d2dd-13df-43ea-85a0-d7eb38dc32ec', // associate
'b3045913-62ac-433e-9211-ac683cdf6b5c', // guest
'63daa0d3-9b63-4434-acff-4977c07808ca', // solo
'e0039285-6667-4f94-80d6-aa6520c6d359', // executive
'4521ce8e-3d24-4b64-9805-59df6f3a4740', // sub-
'ac6f6b4c-a4ec-4483-a04e-9f425a914573', // co-
'288b973a-26ea-4880-8eca-45af4b8e8665', // pre-
'd92884b7-ee0c-46d5-96f3-918196ba8c5b', // vocal
].includes(attribute.id)) return true;
if (!relation.attributes) return false; else if (hasAttribute()) return true; else switch (attribute.id) {
// bass => bass guitar
case '6505f98c-f698-4406-8bf4-8ca43d05c36f':
return ['17f9f065-2312-4a24-8309-6f6dd63e2e33',
'0b9d87fa-93fa-4956-8b6a-a419566cc915'].some(hasAttribute);
}
return false;
});
});
const relationOnTrack = track => hasRelation(track.recording) || getWorks(track).some(hasRelation);
switch (relationLevel) {
case 'recording': case 'work':
if (mbRelease.media) mbRelease.media.forEach(function(medium, mediumIndex) {
const mediumNdx = formData.get(`rel.${index}.medium`);
if (!mediumNdx || parseInt(mediumNdx) == mediumIndex) medium.tracks.forEach(function(track, trackIndex) {
console.assert(track.recording, track);
if (!track.recording) return;
const trackNdx = formData.get(`rel.${index}.track`);
if (!trackNdx || parseInt(trackNdx) == trackIndex) switch (relationLevel) {
case 'recording':
if (!relationOnTrack(track))
edits.push(createEdit({ entityType: relationLevel, gid: track.recording.id }));
break;
case 'work': {
let workResolver = getWorks(track);
if (workResolver.length > 0) {
for (let work of workResolver) if (!hasRelation(work))
edits.push(createEdit({ entityType: relationLevel, gid: work.id }));
break;
}
const findWorkType = (secondaryType, typeId) => typeId > 0
&& mbRelease?.['release-group']?.['secondary-types'].includes(secondaryType) ? typeId : undefined;
const workTypeId = findWorkType('Soundtrack', 22) || findWorkType('Audio drama', 25);
const rxMedley = /^Medley(?:|\s+(?:\(.+\))$)/i;
workResolver = function(id, typeId, name, edit) {
if (workWorkers.has(id)) return workWorkers.get(id);
if (!name) return Promise.reject('Work name is missing');
if (!typeId || [17].includes(typeId)) {
name = trackTitleNorm(name);
//if (rxMedley.test(name)) return Promise.reject('Medley');
}
else name = name.replace(rxBracketStripper(
'live|(?:en|ao) (?:vivo|directo?)|instrumental|acoustic|original|feat(?:\\b|\\.|uring)|ft\\.?',
undefined,
'soundtrack|score|cast',
), '');
let query = `work:"${name}" AND arid:${target}`;
const type = ({ 17: 'song', 22: 'soundtrack', 25: 'audio drama' }[typeId]);
if (type) query += ` AND type:"${type}"`;
const workResolver = mbApiRequest('work', { query: query }).then(function({works}) {
let work = works.filter(work => work.score >= 100);
if (work.length <= 0) work = works.filter(work => sameStringValues(work.title, name)
&& hasRelation(work));
if (work.length <= 0) return Promsie.reject('Not found'); else if (work.length > 1) return null;
work = work[0].relations ? Promise.resolve(work[0]) : mbApiRequest('work/' + work[0].id,
{ inc: 'artist-rels+label-rels+series-rels+place-rels+recording-rels' }).catch(reason => (console.warn(reason), work[0]));
return work.then(work => (reusedWorks.set(work.id, work), work.id));
}).then(workId => workId || Promise.reject('Not resolvable'), function(reason) {
if (!params.createMissingWorks) return Promise.reject(reason);
edit = Object.assign({ comment: '' }, edit, { edit_type: 41, name: name });
if (typeId > 0) edit.type_id = typeId;
return mbCreateEdit([edit], scriptSignature ? 'Auto-created by ' + scriptSignature : undefined, !(params.createMissingWorks >= 2)).then(function([edit]) {
console.assert(edit);
if (edit.response != 1) return Promise.reject('Work create returns error response code ' + edit.response);
notify(`Work <b>${name}</b> successfully created`, 'lime');
console.info('Work', name, 'successfully created (', edit.entity, ')');
return edit.entity.gid;
});
});
workWorkers.set(id, workResolver);
return workResolver;
};
workResolver = workTypeId > 0 ? workResolver(mbRelease.id, workTypeId, mbRelease.title)
: workResolver(track.recording.id, 17, track.recording.title);
// recording <= work relation
edits.push(workResolver.then(function(workId) {
function testForAttribute(expr) {
const rx = new RegExp('\\s+(?:' + ['()', '[]'].map(br => `\\${br[0]}(?:` +
expr + `)\\b[^\\${br[0]}\\${br[1]}]*`).join('|') + ')', 'i');
if ([track.recording.title, track.title, mbRelease.title]
.some(RegExp.prototype.test.bind(rx))) return true;
return new RegExp('^(?:' + expr + ')\\b', 'i').test(track.recording.comment);
}
if (assignedWorks.has(track.recording.id)) return Promise.reject('Work already assigned');
assignedWorks.add(track.recording.id);
const attributes = [ ];
if (mbRelease?.['release-group']?.['secondary-types']?.includes('Live')
|| testForAttribute('live|(?:en|ao) (?:vivo|directo?)'))
attributes.push('70007db6-a8bc-46d7-a770-80e6a0bb551a');
if (testForAttribute('instrumental'))
attributes.push('c031ed4f-c9bb-4394-8cf5-e8ce4db512ae');
if (rxMedley.test(track.recording.title))
attributes.push('37da3398-5d1b-4acb-be25-df95e33e423c');
// attributes.push('3d984f6e-bbe2-4620-9425-5f32e945b60d'); // karaoke
// attributes.push('d2b63be6-91ec-426a-987a-30b47f8aae2d'); // partial
return {
edit_type: 90,
linkTypeID: 278,
entities: [
{ entityType: 'recording', gid: track.recording.id },
{ entityType: 'work', gid: workId },
],
attributes: attributes.length > 0 ?
attributes.map(attributeId => ({ type: { gid: attributeId } })) : null,
};
}).catch(reason => null));
// work <= artist relation
edits.push(workResolver.then(workId => !hasRelation(reusedWorks.get(workId)) ?
createEdit({ entityType: relationLevel, gid: workId })
: Promise.reject('Already related')).catch(reason => null));
break;
}
}
});
});
break;
case 'release':
if (!hasRelation(mbRelease) && (params.overwrite
|| !mbRelease?.media.some(medium => medium?.tracks.some(relationOnTrack))))
edits.push(createEdit({ entityType: 'release', gid: mbRelease.id }));
break;
case 'release-group':
console.assert(mbRelease['release-group'] && mbRelease['release-group'].id, mbRelease);
if (!hasRelation(mbRelease['release-group']) && !hasRelation(mbRelease) && (params.overwrite
|| !mbRelease?.media.some(medium => medium?.tracks.some(relationOnTrack))))
edits.push(createEdit({ entityType: 'release_group', gid: mbRelease['release-group'].id }));
break;
default:
console.warn('Assertion failed, unexpected source entity type:', relationLevel, linkTypeId, targetEntity);
}
}
if (params.updateMetadata) for (let index = 0; formData.has(`urls.${index}.url`); ++index) {
const url = formData.get(`urls.${index}.url`);
const linkType = parseInt(formData.get(`urls.${index}.link_type`));
if (!url || !(linkType > 0)) continue;
const relationLevel = [90, 284].includes(linkType) ? 'release-group' : 'release';
const entity = ({ 'release': mbRelease, 'release-group': mbRelease['release-group'] }[relationLevel]);
if (!entity.relations || !entity.relations.some(function(relation) {
if (relation['target-type'] == 'url') try {
const urls = [url, relation.url.resource].map(url => new URL(url));
return ['hostname', 'pathname'].every(prop => urls[0][prop] == urls[1][prop]);
} catch(e) { console.warn(e) }
return false;
})) edits.push({
edit_type: 90,
linkTypeID: linkType,
entities: [
{ entityType: relationLevel.replace(/-/g, '_'), gid: entity.id },
{ entityType: 'url', name: url },
],
});
}
for (let index = 0; formData.has(`tags.${index}`); ++index) tags.push(formData.get(`tags.${index}`));
tags = tags.filter(Boolean).map(tag => tag.replace(/,\s*(&|and\b)/gi, ' $1').replace(/\s+/g, ' '))
.filter(uniqueValues).filter(tag => !mbRelease.tags.some(rTag =>
sameStringValues(rTag.name, tag)/* && rTag.count > 0*/));
if (tags.length > 0 && GM_getValue('mb_add_tags', true)) {
const url = new URL(['release', mbRelease.id, 'tags', 'upvote'].join('/'), mbOrigin);
url.searchParams.set('tags', tags.join(', '));
globalXHR(url, { responseType: null }).then(function(responseCode) {
notify(tags.length.toString() + ' release tags added with response code ' + responseCode, 'aquamarine');
console.log('%s tags for release %s added with response code', tags.length, mbRelease.id, responseCode);
}, console.error);
}
return edits.length > 0 ? Promise.all(edits).then(function(edits) {
if ((edits = edits.filter(Boolean)).length <= 0) return null;
if (debugLogging) console.debug('Resolved edits:', edits);
// edits = edits.filter((edit1, index, edits) =>
// edits.findIndex(edit2 => objectsEqual(edit1, edit2)) == index);
if (params.simulationMode) return null;
let editNote = ['Release updated'];
if (sourceRef) editNote.push('from ' + sourceRef);
if (scriptSignature) editNote.push('by ' + scriptSignature);
editNote = editNote.length > 0 ? editNote.join(' ') : undefined;
return edits.length > 0 ? mbCreateEdit(edits, editNote, params.makeVotable).then(function(edits) {
console.log('Release', mbRelease.id, (edits.every(edit => edit.response == 1) ?
'edits successfull' : 'edits successfull (some rejected)') + ':', edits);
const allSuccess = edits.every(edit => edit.response == 1);
let message = 'Release edits successfull';
if (allSuccess) message += ' (some rejected)';
notify(message, allSuccess ? 'yellowgreen' : 'darkkhaki');
return edits;
}) : null;
}) : null;
}));
}
function seedToMBIcon(callback, style, tooltip, tooltipster) {
function seedToMB(evt, prompt, required, input) {
if (!(evt instanceof Event)) throw 'Invalid argument';
let target = evt.currentTarget;
if (target instanceof HTMLElement) target.disabled = true; else target = null;
const animation = target && flashElement(target);
(async function(prompt, input) {
let createEntitiesTT = 'Applies to artists, labels and series';
if (!evt.ctrlKey) createEntitiesTT += ' (votable)';
createEntitiesTT += '\nIgnored for AllMusic';
if ((input = await promptEx('Seed new MusicBrainz release', prompt && prompt + ':', required, input, [
['Import tracklist', true],
['Align media with TOCs', false, 'Only meaningful for multivolume releases; when tracklist numbering is missing volume resolution, checking the option tries to detect volumes by aligning with TOCs (logs attached to torrent in sorted sequence required)\nIgnored for AllMusic'],
['Compose annotation', GM_getValue('compose_annotation', true)],
['MBID lookup (required to resolve artists & labels, slower)', true],
['Autocreate missing entities when possible', GM_getValue('mb_create_entities', 1), createEntitiesTT],
['Release group lookup', true],
['Forced recordings lookup', evt.altKey, 'By default recordings lookup is skipped for live albums, DJ mixes and if a release group is found for seeded release'],
], [
['Make edits votable', !(mbSeedNew >= 2)],
['Note upload reference', GM_getValue('insert_upload_reference', false), 'Includes torrent permalink into edit note to improve backward edition verification'],
])) == null) return; else if (!input.input) return callback(target);
let param, id;
if (id = discogsIdExtractor(input.input, 'release')) param = 'discogsId';
else if (id = allMusicIdExtractor(input.input, 'album/release')) param = 'allMusicId';
return param ? callback(target, {
[param]: id,
tracklist: input[0][0], alignWithTOCs: input[0][1],
composeAnnotation: input[0][2],
mbidLookup: input[0][3],
createMissingEntities: input[0][4] ? !input[1][0] && !evt.shiftKey ? 2 : 1 : 0,
rgLookup: input[0][5],
recordingsLookup: input[0][6] ? 2 : 1,
makeVotable: input[1][0],
torrentReference: input[1][1],
}) : (function() {
if (id = mbIdExtractor(input.input, 'release-group')) return Promise.resolve(id);
if (id = mbIdExtractor(input.input, 'release')) return mbApiRequest('release/' + id, { inc: 'release-groups' })
.then(release => release['release-group'].id);
return Promise.reject('Input doesnot contain valid ID/URL');
})().then(releaseGroupId => callback(target, { releaseGroupId: releaseGroupId })/*, reason => callback(target)*/);
})(prompt, input).catch(alert).then(function() {
if (animation != null) animation.cancel();
if (target != null) target.disabled = false;
});
}
const staticIcon = minifyHTML(`
<svg height="0.9em" fill="#0a0" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50"/>
<polygon fill="white" clip-path="circle(30)" points="41.34,0 58.66,0 58.66,41.34 100,41.34 100,58.66 58.66,58.66 58.66,100 41.34,100 41.34,58.66 0,58.66 0,41.34 41.34,41.34"/>
</svg>`);
let prompt = 'Discogs/AllMusic release';
if (!style) prompt += ' / MusicBrainc release group';
prompt += ' ID or URL';
return addIcon(staticIcon, function clickHandler(evt) {
seedToMB(evt, prompt + ' (optional)', false);
}, function dropHandler(evt, urls) {
if (urls = urls.find(url => /^https?:\/\//i.test(url))) seedToMB(evt, prompt, true, urls);
}, 'seed-mb-release', style, tooltip, tooltipster);
}
if (target.disabled) return; else target.disabled = true;
[target.textContent, target.style.color] = ['Looking up...', null];
const animation = flashElement(target);
const getMbTOCs = () => lookupByToc(torrentId, tocEntries => Promise.resolve(tocEntriesToMbTOC(tocEntries)));
const scriptSignature = GM_getValue('signed_edits', true) ?
'Edition lookup by CD TOC browser script (https://greasyfork.org/scripts/459083)' : undefined;
const mbAttachMode = Number(GM_getValue('mb_attach_toc', 2));
const mbSubmitLog = GM_getValue('mb_submit_log', false);
const mbSeedNew = Number(GM_getValue('mb_seed_release', 2));
const mbUpdateRelease = Number(GM_getValue('mb_update_release', 1));
const getDiscogsRels = (entry, entity = 'release') => entry && Array.isArray(entry.relations) ?
entry.relations.filter(relation => relation['target-type'] == 'url' && relation.type == 'discogs')
.map(relation => discogsIdExtractor(relation.url.resource, entity))
.filter((discogsId, index, discogsIds) => discogsId > 0 && discogsIds.indexOf(discogsId) == index) : [ ];
const getAllMusicRels = (entry, entity = 'album/release') => entry && Array.isArray(entry.relations) ?
entry.relations.filter(relation => relation['target-type'] == 'url' && relation.type == 'allmusic')
.map(relation => allMusicIdExtractor(relation.url.resource, entity))
.filter((allMusicId, index, allMusicIds) => allMusicId && allMusicIds.indexOf(allMusicId) == index) : [ ];
const getArtistTracks = mbid => Promise.all([
mbLookupById('recording', 'artist', mbid).then(recordings => recordings.map(recording =>
Object.assign({ level: 'recording', relationType: 'track_artist' }, recording)),
reason => (console.warn(reason), null)),
// mbLookupById('work', 'artist', mbid, ['aliases', 'artist-rels']).then(works => works.filter(work =>
// !work.type || ['Song'].includes(work.type)).map(work =>
// Object.assign({ level: 'work', relationType: work.relations.filter(relation =>
// relation.artist.id == mbid).map(relation => relation.type) }, work)), reason => (console.warn(reason), null)),
mbApiRequest('artist/' + mbid, { inc: 'aliases+recording-rels+work-rels' }).then(function({relations}) {
if (!relations) throw `Assertion failed: no relations for artist ${mbid}`;
return relations.filter(function(relation) {
switch (relation['target-type']) {
case 'recording': return true;
case 'work': return !relation.work.type || ['Song'].includes(relation.work.type);
default: return false;
}
}).map(relation => Object.assign({ level: relation['target-type'], relationType: relation.type },
relation[relation['target-type']]))
.filter((target1, index, tracks) => tracks.findIndex(target2 => ['level', 'relationType', 'id']
.every(prop => target2[prop] == target1[prop])) == index);
}).catch(reason => (console.warn(reason), null)),
]).then(tracks => (tracks = Array.prototype.concat.apply([ ], tracks.filter(Boolean))).length > 0 ? tracks : null);
const normSeedTitle = title => title && [ ].reduce((str, subst) => str.replace(...subst), title);
const capitalizeName = name => name && [
[/\s+/g, ' '],
[/\s+(And|Of|In|On|By|The|Da|De[lnr]?|Du|Van|Von|Y|Et|Vs\.?|Feat(?:uring|\.)?|Ft\.?)(?=\s)/g, (...m) => ' ' + m[1].toLowerCase()],
].reduce((str, subst) => str.replace(...subst), name);
const untitleCase = [/\b(\p{Lu}\p{Ll}+)\b/gu, (...m) => m[1].toLowerCase()];
const bracketStripper = [/(?:\s+(?:\(.+\)|\[.+\]))+$/, ''];
const dateParser = date => date && (date = /^((?:19|20)\d{2})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(date)) != null ?
date.slice(1).map(n => (n = parseInt(n)) > 0 ? n : undefined) : null;
const mbRelationsIndex = {
'release-group': {
// artist
62: 'artists and repertoire', 63: 'creative direction', 65: 'tribute', 868: 'dedicated to', 974: 'named after',
// label
970: 'tribute',
// series
742: 'part of',
},
'release': {
// artist
18: 'art direction', 19: 'design/illustration', 20: 'photography', 22: 'legal representation',
23: 'booking', 24: 'liner notes', 25: 'misc', 26: 'mix', 27: 'graphic design', 28: 'engineer', 29: 'sound',
30: 'producer', 31: 'audio', 32: 'publishing', 36: 'recording', 37: 'programming', 38: 'editor',
40: 'orchestrator', 41: 'instrument arranger', 42: 'mastering', 43: 'mix-DJ', 44: 'instrument',
45: 'performing orchestra', 46: 'conductor', 47: 'remixer', 48: 'compiler', 49: 'samples from artist',
51: 'performer', 53: 'chorus master', 54: 'writer', 55: 'composer', 56: 'lyricist', 57: 'librettist',
60: 'vocal', 295: 'arranger', 296: 'vocal arranger', 709: 'copyright', 710: 'phonographic copyright',
727: 'balance', 759: 'concertmaster', 871: 'translator', 927: 'illustration', 928: 'design',
929: 'booklet editor', 969: 'lacquer cut', 987: 'instrument technician', 993: 'artwork', 1010: 'licensor',
1012: 'field recordist', 1179: 'transfer', 1185: 'video director', 1187: 'audio director',
// label
708: 'copyright', 711: 'phonographic copyright', 833: 'licensee', 712: 'licensor', 362: 'published',
361: 'distributed', 360: 'manufactured', 955: 'glass mastered', 942: 'pressed', 848: 'marketed',
985: 'printed', 359: 'promoted', 349: 'rights society', 948: 'arranged for', 952: 'manufactured for',
1183: 'mastered for', 947: 'mixed for', 951: 'produced for', 999: 'misc', 1174: 'art direction',
1170: 'artwork', 1171: 'design', 1172: 'graphic design', 1173: 'illustration', 1175: 'photography',
// series
741: 'part of',
// place
865: 'arranged at', 812: 'engineered at', 695: 'recorded at', 696: 'mixed at', 697: 'mastered at',
968: 'lacquer cut at', 820: 'edited at', 828: 'remixed at', 1182: 'transferred at', 824: 'produced at',
953: 'manufactured at', 954: 'glass mastered at', 941: 'pressed at',
},
'recording': {
// artist
123: 'photography', 125: 'graphic design', 127: 'publishing', 128: 'recording', 129: 'misc',
130: 'design/illustration', 132: 'programming', 133: 'sound', 134: 'booking', 135: 'artists and repertoire',
136: 'mastering', 137: 'art direction', 138: 'engineer', 140: 'audio', 141: 'producer',
142: 'legal representation', 143: 'mix', 144: 'editor', 146: 'creative direction', 147: 'compiler',
148: 'instrument', 149: 'vocal', 150: 'performing orchestra', 151: 'conductor', 152: 'chorus master',
153: 'remixer', 154: 'samples from artist', 155: 'mix-DJ', 156: 'performer', 158: 'instrument arranger',
297: 'arranger', 298: 'vocal arranger', 300: 'orchestrator', 726: 'balance', 760: 'concertmaster',
858: 'video appearance', 869: 'phonographic copyright', 962: 'video director', 986: 'instrument technician',
1011: 'field recordist', 1186: 'audio director',
// label
867: 'phonographic copyright', 206: 'publishing', 949: 'arranged for', 946: 'mixed for',
950: 'produced for', 1178: 'remixed for', 1228: 'broadcast', 998: 'misc',
// series
740: 'part of', 1006: 'recorded during',
// place
866: 'arranged at', 813: 'engineered at', 693: 'recorded at', 694: 'mixed at', 819: 'edited at',
829: 'remixed at', 825: 'produced at', 963: 'video shot at',
},
'work': {
// artist
167: 'writer', 168: 'composer', 165: 'lyricist', 169: 'librettist', 844: 'revised by', 872: 'translator',
917: 'reconstructed by', 293: 'arranger', 282: 'instrument arranger', 164: 'orchestrator',
294: 'vocal arranger', 834: 'previous attribution', 162: 'misc', 889: 'commissioned', 846: 'dedication',
956: 'premiere', 161: 'publishing', 972: 'named after',
// label
208: 'publishing', 890: 'commissioned', 922: 'dedication',
// series
743: 'part of', 891: 'commissioned',
// place
716: 'premiere', 874: 'written at', 876: 'composed at', 878: 'lyrics written at',
880: 'libretto written at', 882: 'revised at', 883: 'translated at', 886: 'arranged at',
892: 'commissioned', 983: 'dedication',
},
};
const fetchAllInstruments = () => globalXHR(dcOrigin + '/help/creditslist').then(({document}) =>
Array.prototype.filter.call(document.body.querySelectorAll('div#page_content table.table_block > tbody > tr'),
tr => tr.cells[2].textContent.trim() == 'Instruments').map(tr => tr.cells[0].textContent.trim()).filter(instrument => ![
'Guest', 'Performer', 'Soloist', 'Accompanied By', 'Orchestra', 'Ensemble',
'Band', 'Backing Band', 'Brass Band', 'Concert Band', 'Rhythm Section',
].includes(instrument)));
const instrumentMapper = (attributes, creditType, creditedAs) => [44, 148].map(function(linkTypeId) {
const relation = { linkTypeId: linkTypeId };
if (creditType) relation.creditType = creditType;
if (!attributes) attributes = [ ];
if (/^(?:Guest)\b/i.test(creditedAs)) attributes.push({ id: 'b3045913-62ac-433e-9211-ac683cdf6b5c' });
else if (/^(?:Solo(?:ist)?)$/i.test(creditedAs)) attributes.push({ id: '63daa0d3-9b63-4434-acff-4977c07808ca' });
else if (/^(?:Additional)\b/i.test(creditedAs)) attributes.push({ id: '0a5341f8-3b1d-4f99-a0c6-26b7f4e42c7f' });
else if (creditedAs) attributes = attributes.map(attribute =>
Object.assign({ }, attribute, { creditedAs: creditedAs.replace(...untitleCase) }));
if (attributes.length > 0) relation.attributes = attributes;
return relation;
});
const isTrackLevel = level => ['recording', 'work'].includes(level);
const isReleaseLevel = level => ['release-group', 'release'].includes(level);
const findRelationLevel = linkTypeId => Object.keys(mbRelationsIndex).find(key => linkTypeId in mbRelationsIndex[key]);
const mbCreateEdit = (edits, editNote, makeVotable = true) => globalXHR(mbOrigin + '/ws/js/edit/create', { responseType: 'json' }, {
edits: edits,
editNote: editNote || null,
makeVotable: makeVotable,
}).then(({response}) => response.edits);
lookupByToc(torrentId, (tocEntries, discNdx, totalDiscs) =>
mbLookupByDiscID(tocEntriesToMbTOC(tocEntries), !evt.ctrlKey)).then(function(results) {
if (animation) animation.cancel();
if (logScoresCache && 'torrentId' in logScoresCache && logScoresCache[torrentId].every(uncalibratedReadOffset))
return Promise.reject('Incorrect read offset');
if (mbSeedNew > 0) target.after(seedToMBIcon((target, params) => queryAjaxAPICached('torrent', { id: torrentId })
.then(torrent => seedToMB(target, torrent, params), alert), undefined, `Seed new MusicBrainz release from this CD TOC
Drop Discogs/AllMusic release link to import external metadata
or drop exising MusicBrainz release group link to add to this group
MusicBrainz account required`, true));
if (mbAttachMode > 0) target.after(attachToMBIcon(undefined, undefined,
'Attach this CD TOC by hand to release not shown in lookup results\nor drop release link here\nMusicBrainz account required', true));
let score = results.every(medium => medium == null) ? 8 : results[0] == null ?
results.every(medium => medium == null || !medium.attached) ? 7 : 6 : 5;
if (score < 6 || !evt.ctrlKey) target.dataset.haveResponse = true;
if (score > 7) return Promise.reject('No matches'); else if (score > 5) {
target.textContent = 'Unlikely matches';
target.style.color = score > 6 ? '#f40' : '#f80';
setTooltip(target, Boolean(target.dataset.haveResponse) ?
`Matched media found only for some volumes (${score > 6 ? 'fuzzy' : 'exact'})` : null);
return;
}
let releases = results[0].releases.filter(release => !release.media
|| sameMedia(release).length == results.length, results);
if (releases.length > 0) score = results.every(result => result != null) ?
results.every(result => result.attached) ? 0 : results.some(result => result.attached) ? 1 : 2
: results.some(result => result != null && result.attached) ? 3 : 4;
if (releases.length <= 0) releases = results[0].releases;
target.dataset.releaseIds = JSON.stringify(releases.map(release => release.id));
target.dataset.discIds = JSON.stringify(results.filter(Boolean).map(result => result.mbDiscID));
target.dataset.tocs = JSON.stringify(results.filter(Boolean).map(result => result.mbTOC));
setTooltip(target, 'Open results in new window');
(function(type, color) {
type = `${releases.length} ${type} match`;
target.textContent = releases.length != 1 ? type + 'es' : type;
target.style.color = color;
})(...[
['exact', '#0a0'], ['hybrid', '#3a0'], ['fuzzy', '#6a0'],
['partial', '#9a0'], ['partial', '#ca0'], ['irrelevant', '#f80'],
][score]);
if (autoOpenTab && score < 2) GM_openInTab(mbOrigin + '/cdtoc/' +
(evt.shiftKey ? 'attach?toc=' + results[0].mbTOC.join(' ') : results[0].mbDiscID), true);
if (score < 5) return queryAjaxAPICached('torrent', { id: torrentId }).then(function(torrent) {
function buildEditionTitle(release) {
const editionTitle = (release.disambiguation || '')
.split(/[\,\;]/).map(str => str.trim()).filter(Boolean);
if (release.status) switch (release.status) {
case 'Official': break;
case 'Bootleg': editionTitle.push('Unofficial'); break;
default: editionTitle.push(release.status);
}
// if (release.packaging && !['Jewel Case'].includes(release.packaging))
// editionTitle.push(release.packaging);
Array.prototype.push.apply(editionTitle, release.media.filter(isCD)
.map(medium => medium.format).filter(format => format != 'CD').filter(uniqueValues));
if (useCountryInTitle && release.country && !['XW'].includes(release.country.toUpperCase()))
editionTitle.push((iso3166ToCountryShort[release.country.toUpperCase()] || release.country).toUpperCase());
return editionTitle.length > 0 ? editionTitle.join(' / ') : undefined;
}
const isCompleteInfo = torrent.torrent.remasterYear > 0
&& Boolean(torrent.torrent.remasterRecordLabel)
&& Boolean(torrent.torrent.remasterCatalogueNumber);
const is = what => !torrent.torrent.remasterYear && ({
unknown: torrent.torrent.remastered,
unconfirmed: !torrent.torrent.remastered,
}[what]);
if (torrent.torrent.description)
torrentDetails.dataset.torrentDescription = torrent.torrent.description.trim();
// add inpage search results
const [thead, table, tbody] = createElements('div', 'table', 'tbody');
[thead.style, thead.innerHTML] = [theadStyle, `<b>Applicable MusicBrainz matches</b> (${[
'exact',
`${results.filter(result => result != null && result.attached).length} exact out of ${results.length} matches`,
'fuzzy',
`${results.filter(result => result != null && result.attached).length} exact / ${results.filter(result => result != null).length} matches out of ${results.length}`,
`${results.filter(result => result != null).length} matches out of ${results.length}`,
][score]})`];
table.style = 'margin: 0; max-height: 10rem; overflow-y: auto; empty-cells: hide;';
table.className = 'mb-lookup-results mb-lookup-' + torrent.torrent.id;
const [recordLabels, catalogueNumbers] = editionInfoParser(torrent.torrent);
const labelInfoMapper = release => Array.isArray(release['label-info']) ?
release['label-info'].map(labelInfo => ({
label: labelInfo.label ? rxNoLabel.test(labelInfo.label.name) ? noLabel : labelInfo.label.name : undefined,
catNo: rxNoCatno.test(labelInfo['catalog-number']) ? undefined : labelInfo['catalog-number'],
})).filter(labelInfo => labelInfo.label || labelInfo.catNo) : [ ];
const rowWorkers = [ ];
releases.forEach(function(release, index) {
const [tr, artist, title, releaseEvents, editionInfo, barcode, groupSize, releasesWithId] =
createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td', 'td');
[tr.className, tr.style] = ['musicbrainz-release', 'transition: color 200ms ease-in-out;'];
if (release.quality == 'low') tr.style.opacity = 0.75;
[releaseEvents, barcode].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[groupSize, releasesWithId].forEach(elem => { elem.style.textAlign = 'right' });
setMusicBrainzArtist(release, artist);
setMusicBrainzTitle(release, title);
// attach CD TOC
if (mbAttachMode > 0 && (score > 0 || results.some(medium => !medium.releases.some(_release =>
_release.id == release.id)))) title.prepend(attachToMBIcon(release.id,
'float: right; margin: 0 0 0 4pt;',
`Attach CD TOC to release (verify CD rip and MB release are identical edition)
Submission mode: ${mbAttachMode > 1 ? 'unattended (Alt+click enforces attended mode, Ctrl+click disables poll)' : 'attended'}
MusicBrainz account required`));
// Seed new edition
if (mbSeedNew > 0) title.prepend(seedToMBIcon((target, params) =>
seedToMB(target, torrent, Object.assign((params || { }), { releaseGroupId: release['release-group'].id })),
'float: right; margin: 0 0 0 4pt;', `Seed new MusicBrainz edition from this CD TOC in same release group
Drop Discogs/AllMusic release link to import external metadata
MusicBrainz account required`));
// update from external DB
if (mbUpdateRelease > 0) {
function updateFromXtrnDb(evt, prompt, input) {
if (!(evt instanceof Event)) throw 'Invalid argument';
let target = evt.currentTarget;
if (target instanceof HTMLElement) target.disabled = true; else target = null;
const animation = target && flashElement(target);
(async function(prompt, input) {
let title = 'Update MusicBrainz release';
if (evt.altKey) title += ' (simulation mode)';
let createEntitiesTT = 'Applies to artists, labels, series and places';
if (!evt.ctrlKey) createEntitiesTT += ' (votable)';
createEntitiesTT += '\nIgnored on AllMusic';
if ((input = await promptEx(title, prompt + ':', true, input, [
['Update release metadata', true],
['Compose annotation', GM_getValue('compose_annotation', true)],
['Create release-level relationships', true, 'Release and release group'],
['Create track-level relationships', true, 'Recording and work'],
['Prefer track level over release level relationships where applicable', false, 'Option affecting how to import entities related to release without specific track bindings. Relations available for both of release and track level will be created for all single tracks insted of for release. Relationships available only at release or track level will be created at that level regardless the option.\nActivate this option only if sure that all relations listed for release are also valid for each single track.'],
['Autocreate missing entities when possible', GM_getValue('mb_create_entities', 1), createEntitiesTT],
['Autocreate new works', GM_getValue('mb_create_works', 1), 'Required to assign new writing credits on track level'],
['Align media with TOCs', false, 'Only meaningful for multivolume releases; when tracklist numbering is missing volume resolution, checking the option tries to detect volumes by aligning with TOCs (logs attached in ordered sequence required)'],
['Overwrite existing values (not recommended, use with caution)', false],
['Make edits votable', !(mbUpdateRelease >= 2)],
])) == null) return;
let param, xtrnDbId;
if (xtrnDbId = discogsIdExtractor(input.input, 'release')) param = 'discogsId';
else if (xtrnDbId = allMusicIdExtractor(input.input, 'album/release')) param = 'allMusicId';
else throw 'Input doesnot contain valid ID/URL';
const updateFromXtrnDb = (seeder, dbName) => updateFromExternalDb(release.id,
(params, cdLengths) => seeder(new URLSearchParams, xtrnDbId, {
mbidLookup: true, extendedMetadata: true,
composeAnnotation: input[0][1],
releaseRelations: input[0][2], rgRelations: input[0][2],
recordingRelations: input[0][3], workRelations: input[0][3],
preferTrackRelations: input[0][4],
createMissingEntities: input[0][5] && !evt.altKey ? !input[0][9] && !evt.shiftKey ? 2 : 1 : 0,
alignWithTOCs: input[0][7],
languageIdentifier: params.languageIdentifier,
tracklist: false, recordingsLookup: 0, rgLookup: false, lookupArtistsByRecording: false,
}, cdLengths), `${dbName} release id ${xtrnDbId} (${({
discogsId: [dcOrigin, 'release', xtrnDbId].join('/'),
allMusicId: 'https://www.allmusic.com/album/release/' + xtrnDbId,
}[param])})`, {
updateMetadata: input[0][0],
createMissingWorks: input[0][6] && !evt.altKey ? !input[0][9] && !evt.shiftKey ? 2 : 1 : 0,
overwrite: input[0][8],
makeVotable: input[0][9],
simulationMode: evt.altKey,
}).then(function(results) {
if (results === null) return alert('Nothing to be updated');
if (results) GM_openInTab([mbOrigin, 'release', release.id,
/*mbUpdateRelease > 1 ? 'edit' : */'edits'].join('/'), false);
});
switch (param) {
case 'discogsId': return updateFromXtrnDb(seedFromDiscogs, 'Discogs');
case 'allMusicId': return updateFromXtrnDb(seedFromAllMusic, 'AllMusic');
default: throw 'Method not implemented';
}
})(prompt, input).catch(alert).then(function() {
if (animation != null) animation.cancel();
if (target != null) target.disabled = false;
});
}
let prompt = 'Discogs/AllMusic release ID or URL';
title.prepend(addIcon(minifyHTML(`
<svg fill="#0a8" height="0.9em" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M53.34 26.69c-2.26,-0.18 -6.1,-0.03 -7.24,0.16 -7.03,1.17 -13.53,5.68 -16.97,11.94 -1.86,3.38 -2.67,6.52 -2.84,10.38 -0.14,3.33 0.22,6.51 1.51,9.6l0.58 1.41 -0.12 0.58 -4.45 4.4c-0.74,0.73 -4.48,5.3 -5.88,3.9 -0.49,-0.66 -1.12,-2.12 -1.47,-2.83 -3.61,-7.38 -4.59,-15.72 -2.9,-23.75 2.7,-12.84 12.32,-23.33 24.76,-27.42 4.34,-1.42 6.82,-1.71 11.41,-1.71 0.53,0 1.57,0.03 2.49,0.02 -0.51,-0.65 -1.07,-1.3 -1.51,-1.83 -0.62,-0.77 -2.1,-2.53 -2.52,-3.38l-0.05 -0.1 -0.05 -0.21 0 -0.1c-0.03,-0.94 8.59,-7.79 9.66,-7.79l0.22 0 0.4 0.19 0.14 0.18c3.07,3.83 6.07,7.74 9.07,11.61 1.02,1.31 8.83,11.06 9.18,12.09l0.02 0.08 0.03 0.16 0 0.08c0,0.9 -23.51,19.18 -24.47,19.18 -0.88,0 -8.01,-9.05 -7.48,-10.1 0.26,-0.52 4.49,-3.7 5.1,-4.16 0.99,-0.77 2.27,-1.68 3.38,-2.58z"/>
<path d="M50.29 87.29c1.05,1.32 2.26,2.69 3.28,3.84 1.48,1.66 -2.96,4.48 -4.01,5.35 -0.88,0.73 -4.55,4.48 -5.8,3.23 -3.13,-3.11 -6.8,-8 -9.71,-11.46 -2.55,-3.04 -5.15,-6.08 -7.63,-9.18 -0.58,-0.74 -2.53,-2.72 -1.25,-3.59 3.79,-2.97 7.6,-6.31 11.33,-9.44 1.03,-0.86 11.04,-9.54 11.85,-9.54 0.9,0 7.22,7.53 7.89,8.71 0.63,1.27 -4.47,4.95 -5.22,5.58 -0.86,0.71 -2.39,1.9 -3.56,2.95 0.13,0.01 0.25,0.01 0.35,0.02 3.35,0.31 7.62,-0.23 10.74,-1.47 7.82,-3.13 13.5,-9.94 14.95,-18.26 0.54,-3.14 0.28,-7.43 -0.64,-10.48l-0.45 -1.51 0.13 -0.53 4.52 -4.43c0.59,-0.59 4.29,-4.56 5.05,-4.56 0.94,0 1.49,1.63 1.81,2.32 5.26,11.59 4.22,25.13 -2.82,35.74 -4.63,6.97 -11.17,11.94 -19.09,14.64 -3.31,1.13 -6.61,1.67 -10.09,1.92 -0.51,0.04 -1.07,0.1 -1.63,0.15z"/>
</svg>`), evt => { updateFromXtrnDb(evt, prompt + ' (required)') }, function dropHandler(evt, urls) {
if (urls = urls.find(url => /^https?:\/\//i.test(url))) updateFromXtrnDb(evt, prompt, urls);
}, 'update-from-xtrn-source', 'float: right; margin: 0 0 0 2pt;', 'Update release info from external source and create relations\n(drop Discogs/AllMusic release link here)\nMusicBrainz account required'));
}
setMusicBrainzReleaseEvents(release, releaseEvents, torrent.torrent.remasterYear);
if (Array.isArray(release['label-info'])) fillListRows(editionInfo, release['label-info']
.map(labelInfo => editionInfoMapper(labelInfo.label && labelInfo.label.name,
labelInfo['catalog-number'], recordLabels, catalogueNumbers,
labelInfo.label && labelInfo.label.id && labelInfo.label.id != mb.spl.noLabel ?
[mbOrigin, 'label', labelInfo.label.id].join('/') : undefined)));
if (editionInfo.childElementCount <= 0) mbFindEditionInfoInAnnotation(editionInfo, release.id);
if (release.barcode) {
barcode.textContent = release.barcode;
if (catalogueNumbers.some(catalogueNumber => sameBarcodes(catalogueNumber, release.barcode)))
editionInfoMatchingStyle(barcode);
}
setMusicBrainzGroupSize(release, groupSize, releasesWithId, results.length);
tr.dataset.releaseUrl = [mbOrigin, 'release', release.id].join('/');
const releaseYear = getReleaseYear(release.date), _editionInfo = labelInfoMapper(release);
if (!_editionInfo.some(labelInfo => labelInfo.catNo) && release.barcode)
_editionInfo.push({ catNo: release.barcode });
if (releaseYear > 0 && _editionInfo.length > 0) {
tr.dataset.remasterYear = releaseYear;
setEditionInfo(tr, _editionInfo);
let editionTitle = buildEditionTitle(release);
if (editionTitle) tr.dataset.remasterTitle = editionTitle;
try {
if (isCompleteInfo || !('editionGroup' in torrentDetails.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey)
throw 'Not applicable';
if (!(releaseYear > 0)) throw 'Edition year missing';
if (_editionInfo.length <= 0 && torrent.torrent.remasterYear > 0
&& (torrent.torrent.remasterTitle || !editionTitle)) throw 'No additional edition information';
applyOnClick(tr);
} catch(e) { applyOnCtrlClick(tr) }
}
setMusicBrainzTooltip(release, tr);
tr.append(artist, title, releaseEvents, editionInfo, barcode, groupSize, releasesWithId);
for (let cell of tr.cells) cell.style.backgroundColor = 'inherit';
['artist', 'title', 'release-events', 'edition-info', 'barcode', 'editions-total', 'discids-total']
.forEach((className, index) => { tr.cells[index].className = className });
tbody.append(tr);
for (let discogsId of getDiscogsRels(release)) {
if (title.querySelector('span.have-discogs-relatives') == null) {
const span = document.createElement('span');
[span.className, span.innerHTML] = ['have-discogs-relatives', GM_getResourceText('dc_icon')];
span.firstElementChild.setAttribute('height', 6);
span.firstElementChild.removeAttribute('width');
span.firstElementChild.style.verticalAlign = 'top';
svgSetTitle(span.firstElementChild, 'Has defined Discogs relative(s)');
title.append(' ', span);
}
rowWorkers.push(dcApiRequest('releases/' + discogsId).then(function(release) {
const [trDC, icon, artist, title, releaseEvents, editionInfo, barcode, groupSize] =
createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td', 'td');
[trDC.className, icon.style.textAlign] = ['discogs-release', 'right'];
trDC.style = 'transition: color 200ms ease-in-out;';
trDC.style.backgroundColor = trDC.dataset.backgroundColor ='#8881';
[barcode.style.whiteSpace, groupSize.style.textAlign] = ['nowrap', 'right'];
setDiscogsArtist(artist, release.artists);
title.innerHTML = linkHTML(release.uri, release.title, 'discogs-release');
let descriptors = getDiscogsReleaseDescriptors(release);
if (descriptors.length > 0) appendDisambiguation(title, descriptors.join(', '));
if (release.images && release.images.length > 0) {
let thumbnail = release.images.find(image => image.type == 'primary') || release.images[0];
addThumbnail(title, thumbnail && (thumbnail.uri150 || thumbnail.uri),
[dcOrigin, 'release', release.id, 'images'].join('/'));
}
if (release.country || release.released)
fillListRows(releaseEvents, iso3166ToFlagCodes(discogsCountryToIso3166Mapper(release.country)).map(countryCode =>
releaseEventMapper(countryCode, release.released && release.released.replace(/(?:-00)+$/, ''),
torrent.torrent.remasterYear)), 3);
if (Array.isArray(release.labels)) fillListRows(editionInfo, release.labels.map(label =>
editionInfoMapper(stripDiscogsNameVersion(label.name), label.catno, recordLabels, catalogueNumbers,
label.id && !label.name.startsWith('Not On Label') ? [dcOrigin, 'label', label.id].join('/') : undefined)));
let barCode = release.identifiers && release.identifiers.find(id => id.type == 'Barcode');
if (barCode && (barCode = barCode.value.replace(/\D+/g, ''))) {
barcode.textContent = barCode;
if (catalogueNumbers.some(catalogueNumber => sameBarcodes(catalogueNumber, barCode)))
editionInfoMatchingStyle(barcode);
}
setDiscogsGroupSize(release, groupSize);
icon.innerHTML = GM_getResourceText('dc_icon');
icon.firstElementChild.style = '';
icon.firstElementChild.removeAttribute('width');
icon.firstElementChild.setAttribute('height', '0.9em');
svgSetTitle(icon.firstElementChild, release.id);
trDC.dataset.releaseUrl = [dcOrigin, 'release', release.id].join('/');
const releaseYear = getReleaseYear(release.released);
const _editionInfo = Array.isArray(release.labels) ? release.labels.map(label => ({
label: rxNoLabel.test(label.name) ? noLabel : stripDiscogsNameVersion(label.name),
catNo: rxNoCatno.test(label.catno) ? undefined : label.catno,
})).filter(label => label.label || label.catNo) : [ ];
if (!_editionInfo.some(label => label.catNo) && barCode) _editionInfo.push({ catNo: barCode });
if (releaseYear > 0 && _editionInfo.length > 0) {
trDC.dataset.remasterYear = releaseYear;
setEditionInfo(trDC, _editionInfo);
descriptors = Array.from(descriptors).filter(description => ![
'Mini-Album', 'Digipak', 'Digipack', 'Sampler', 'Mixtape', 'CD-TEXT', 'DVD', 'Reissue', //'Maxi-Single',
].includes(description));
if (release.formats) {
if (release.formats.some(format => format.name == 'CDr')) descriptors.push('CD-R');
if (release.formats.some(format => format.name == 'SACD') && descriptors.includes('Hybrid'))
descriptors.push('Hybrid SACD');
if (release.formats.some(format => format.name == 'Hybrid') && descriptors.includes('DualDisc'))
descriptors.push('DualDisc');
}
const countriesIso3166 = discogsCountryToIso3166Mapper(release.country)
.filter(countryIso3166 => countryIso3166 && !['XW'].includes(countryIso3166));
if (useCountryInTitle && countriesIso3166.length > 0 && countriesIso3166.length < 3)
descriptors.push(countriesIso3166.map(countryIso3166 =>
iso3166ToCountryShort[countryIso3166] || countryIso3166).join(' / '));
if (descriptors.length > 0) trDC.dataset.remasterTitle = descriptors.join(' / ');
try {
if (isCompleteInfo || !('editionGroup' in torrentDetails.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey)
throw 'No additional edition information';
if (!(releaseYear > 0)) throw 'Edition year missing';
if (_editionInfo.length <= 0 && torrent.torrent.remasterYear > 0
&& (torrent.torrent.remasterTitle || !descriptors))
throw 'No additional edition information';
applyOnClick(trDC);
} catch(e) { applyOnCtrlClick(trDC) }
}
setDiscogsTooltip(release, trDC);
trDC.append(artist, title, releaseEvents, editionInfo, barcode, groupSize, icon);
for (let cell of trDC.cells) cell.style.backgroundColor = 'inherit';
['artist', 'title', 'release-events', 'edition-info', 'barcode', 'editions-total', 'discogs-icon']
.forEach((className, index) => { trDC.cells[index].className = className });
tr.after(trDC); //tbody.append(trDC);
}, reason => { svgSetTitle(title.querySelector('span.have-discogs-relatives').firstElementChild, reason) }));
}
});
table.append(tbody);
Promise.all(rowWorkers).then(() => { addResultsFilter(thead, tbody, 5) }, console.warn);
addLookupResults(torrentId, thead, table);
// Group set
if (isCompleteInfo || !('editionGroup' in torrentDetails.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey
|| torrent.torrent.remasterYear > 0 && !(releases = releases.filter(release =>
!release.date || getReleaseYear(release.date) == torrent.torrent.remasterYear))
.some(release => release['label-info'] && release['label-info'].length > 0 || release.barcode)
|| releases.length > (is('unknown') ? 1 : 2)) return;
const releaseYear = releases.reduce((year, release) =>
year > 0 ? year : getReleaseYear(release.date), undefined);
if (!(releaseYear > 0) || releases.some(release1 => releases.some(release2 =>
getReleaseYear(release2.date) != getReleaseYear(release1.date)))
|| !releases.every((release, ndx, arr) =>
release['release-group'].id == arr[0]['release-group'].id)) return;
const a = document.createElement('a');
[a.className, a.href, a.textContent, a.style.fontWeight] =
['update-edition', '#', '(set)', score <= 0 && releases.length < 2 ? 'bold' : 300];
let editionInfo = Array.prototype.concat.apply([ ], releases.map(labelInfoMapper)), editionTitle;
const barcodes = releases.map(release => release.barcode).filter(Boolean);
if (!editionInfo.some(labelInfo => labelInfo.catNo) && barcodes.length > 0)
Array.prototype.push.apply(editionInfo, barcodes.map(barcode => ({ catNo: barcode })));
if (releases.length < 2) editionTitle = buildEditionTitle(releases[0]);
if (editionInfo.length <= 0 && torrent.torrent.remasterYear > 0
&& (torrent.torrent.remasterTitle || !editionTitle)) return;
a.dataset.remasterYear = releaseYear;
setEditionInfo(a, editionInfo);
if (editionTitle) a.dataset.remasterTitle = editionTitle;
if (releases.length < 2) a.dataset.releaseUrl = [mbOrigin, 'release', releases[0].id].join('/');
setTooltip(a, 'Update edition info from matched release(s)\n\n' + releases.map(release =>
release['label-info'].map(labelInfo => [getReleaseYear(release.date), [
labelInfo.label && labelInfo.label.name,
labelInfo['catalog-number'] || release.barcode,
].filter(Boolean).join(' / ')].filter(Boolean).join(' – ')).filter(Boolean).join('\n')).join('\n'));
a.onclick = updateEdition;
if (is('unknown') || releases.length > 1) a.dataset.confirm = true;
target.after(a);
}, alert);
}).catch(function(reason) {
if (animation) animation.cancel();
target.textContent = reason;
target.style.color = 'red';
if (Boolean(target.dataset.haveResponse)) setTooltip(target);
}).then(() => { target.disabled = false });
}
}, 'Lookup edition on MusicBrainz by Disc ID/TOC (Ctrl enforces strict TOC matching)\nUse Alt to lookup by CDDB ID');
addLookup('GnuDb', function(evt) {
const target = evt.currentTarget;
console.assert(target instanceof HTMLElement);
const entryUrl = entry => `https://gnudb.org/cd/${entry[1].slice(0, 2)}${entry[2]}`;
if (Boolean(target.dataset.haveResponse)) {
if (!('entries' in target.dataset)) return;
for (let entry of JSON.parse(target.dataset.entries).reverse()) GM_openInTab(entryUrl(entry), false);
return;
} else if (target.disabled) return; else target.disabled = true;
target.textContent = 'Looking up...';
target.style.color = null;
lookupByToc(parseInt(torrentDetails.dataset.torrentId), function(tocEntries) {
console.info('Local CDDB ID:', getCDDBiD(tocEntries));
console.info('Local AR ID:', getARiD(tocEntries));
const reqUrl = new URL('https://gnudb.gnudb.org/~cddb/cddb.cgi');
let tocDef = [tocEntries.length].concat(tocEntries.map(tocEntry => preGap + tocEntry.startSector));
const tt = preGap + tocEntries[tocEntries.length - 1].endSector + 1 - tocEntries[0].startSector;
tocDef = tocDef.concat(Math.floor(tt / msf)).join(' ');
reqUrl.searchParams.set('cmd', `discid ${tocDef}`);
reqUrl.searchParams.set('hello', `name ${document.domain} userscript.js 1.0`);
reqUrl.searchParams.set('proto', 6);
return globalXHR(reqUrl, { responseType: 'text' }).then(function({responseText}) {
console.log('GnuDb CDDB discid:', responseText);
const response = /^(\d+) Disc ID is ([\da-f]{8})$/i.exec(responseText.trim());
if (response == null) return Promise.reject(`Unexpected response format (${responseText})`);
console.assert((response[1] = parseInt(response[1])) == 200);
reqUrl.searchParams.set('cmd', `cddb query ${response[2]} ${tocDef}`);
return globalXHR(reqUrl, { responseType: 'text', context: response });
}).then(function({responseText}) {
console.log('GnuDb CDDB query:', responseText);
let entries = /^(\d+)\s+(.+)/.exec((responseText = responseText.trim().split(/\r?\n/))[0]);
if (entries == null) return Promise.reject('Unexpected response format');
const statusCode = parseInt(entries[1]);
if (statusCode < 200 || statusCode >= 400) return Promise.reject(`Server response error (${statusCode})`);
if (statusCode == 202) return Promise.reject('No matches');
entries = (statusCode >= 210 ? responseText.slice(1) : [entries[2]])
.map(RegExp.prototype.exec.bind(/^(\w+)\s+([\da-f]{8})\s+(.*)$/i)).filter(Boolean);
return entries.length <= 0 ? Promise.reject('No matches')
: { status: statusCode, discId: arguments[0].context[2], entries: entries };
});
}).then(function(results) {
if (logScoresCache && 'torrentId' in logScoresCache && logScoresCache[torrentId].every(uncalibratedReadOffset))
return Promise.reject('Incorrect read offset');
if (results.length <= 0 || results[0] == null) return Promise.reject('No matches');
let caption = `${results[0].entries.length} ${['exact', 'fuzzy'][results[0].status % 10]} match`;
if (results[0].entries.length > 1) caption += 'es';
target.textContent = caption;
target.style.color = '#0a0';
if (results[0].entries.length <= 5) for (let entry of Array.from(results[0].entries).reverse())
GM_openInTab(entryUrl(entry), true);
target.dataset.entries = JSON.stringify(results[0].entries);
target.dataset.haveResponse = true;
}).catch(function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
}, 'Lookup edition on GnuDb (CDDB disc id)');
addLookup('CTDB', function(evt) {
const target = evt.currentTarget;
console.assert(target instanceof HTMLElement);
if (target.disabled) return; else target.disabled = true;
const torrentId = parseInt(torrentDetails.dataset.torrentId);
if (!(torrentId > 0)) throw 'Assertion failed: invalid torrentId';
lookupByToc(torrentId, function(tocEntries) {
if (tocEntries.length > 100) throw 'TOC size exceeds limit';
tocEntries = tocEntries.map(tocEntry => tocEntry.endSector + 1 - tocEntries[0].startSector);
return Promise.resolve(new DiscID().addData(tocEntries, 8, 100).digest);
}).then(function(tocIds) {
if (!Boolean(target.dataset.haveQuery) && !autoOpenTab) return;
for (let tocId of Array.from(tocIds).reverse()) if (tocId != null)
GM_openInTab('https://db.cue.tools/?tocid=' + tocId, !Boolean(target.dataset.haveQuery));
}, function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
if (Boolean(target.dataset.haveQuery)) return;
const ctdbLookup = params => lookupByToc(torrentId, function(tocEntries, volumeNdx) {
const url = new URL('https://db.cue.tools/lookup2.php');
url.searchParams.set('version', 3);
url.searchParams.set('ctdb', 1);
if (params) for (let param in params) url.searchParams.set(param, params[param]);
url.searchParams.set('toc', tocEntries.map(tocEntry => tocEntry.startSector)
.concat(tocEntries.pop().endSector + 1).join(':'));
const saefInt = (base, property) =>
isNaN(property = parseInt(base.getAttribute(property))) ? undefined : property;
return globalXHR(url).then(({responseXML}) => ({
metadata: Array.from(responseXML.getElementsByTagName('metadata'), metadata => ({
source: metadata.getAttribute('source') || undefined,
id: metadata.getAttribute('id') || undefined,
artist: metadata.getAttribute('artist') || undefined,
album: metadata.getAttribute('album') || undefined,
year: saefInt(metadata, 'year'),
discNumber: saefInt(metadata, 'discnumber'),
discCount: saefInt(metadata, 'disccount'),
release: Array.from(metadata.getElementsByTagName('release'), release => ({
date: release.getAttribute('date') || undefined,
country: release.getAttribute('country') || undefined,
})),
labelInfo: Array.from(metadata.getElementsByTagName('label'), label => ({
name: label.getAttribute('name') || undefined,
catno: label.getAttribute('catno') || undefined,
})),
barcode: metadata.getAttribute('barcode') || undefined,
infourl: metadata.getAttribute('infourl') || undefined,
extra: Array.from(metadata.getElementsByTagName('extra'), extra => extra.textContent.trim()),
relevance: saefInt(metadata, 'relevance'),
})),
entries: Array.from(responseXML.getElementsByTagName('entry'), entry => ({
confidence: saefInt(entry, 'confidence'),
crc32: saefInt(entry, 'crc32'),
hasparity: entry.getAttribute('hasparity') || undefined,
id: saefInt(entry, 'id'),
npar: saefInt(entry, 'npar'),
stride: saefInt(entry, 'stride'),
syndrome: entry.getAttribute('syndrome') || undefined,
toc: entry.hasAttribute('toc') ?
entry.getAttribute('toc').split(':').map(offset => parseInt(offset)) : undefined,
trackcrcs: entry.hasAttribute('trackcrcs') ?
entry.getAttribute('trackcrcs').split(' ').map(crc => parseInt(crc, 16)) : undefined,
})),
}));
}).then(function(results) {
console.log('CTDB lookup (%s, %d) results:', params.metadata, params.fuzzy, results);
return results.length > 0 && results[0] != null && (results = Object.assign(results[0].metadata.filter(function(metadata) {
if (!['musicbrainz', 'discogs'].includes(metadata.source)) return false;
if (metadata.discCount > 0 && metadata.discCount != results.length) return false;
return true;
}), { confidence: (entries => getSessions(torrentId).then(sessions => sessions.length == entries.length ? sessions.map(function(session, volumeNdx) {
if (rxRangeRip.test(session)) return null;
const rx = [
/^\s+(?:(?:Copy|复制|Kopie|Copia|Kopiera|Copiar|Копиран) CRC|CRC (?:копии|êîïèè|kopii|kopije|kopie|kópie)|コピーCRC)\s+([\da-fA-F]{8})$/gm,
/^\s+(?:CRC32 hash|CRC)\s*:\s*([\da-fA-F]{8})$/gm, // XLD / EZ CD
];
return (session = session.match(rx[0]) || session.match(rx[1])) && session.map(match =>
parseInt(rx.reduce((m, rx) => m || (rx.lastIndex = 0, rx.exec(match)), null)[1], 16));
}).map(function getScores(checksums, volumeNdx) {
if (checksums == null || entries[volumeNdx] == null || checksums.length < 3
|| !entries[volumeNdx].some(entry => entry.trackcrcs && entry.trackcrcs.length == checksums.length))
return null; // tracklist too short
const getMatches = matchFn => entries[volumeNdx].reduce((sum, entry, ndx) =>
matchFn(entry.trackcrcs && entry.trackcrcs.length == checksums.length ?
entry.trackcrcs.slice(1, -1).filter((crc32, ndx) =>
crc32 == checksums[ndx + 1]).length / (entry.trackcrcs.length - 2) : -Infinity) ?
sum + entry.confidence : sum, 0);
return [entries[volumeNdx].reduce((sum, entry) => sum + entry.confidence, 0),
getMatches(score => score >= 1), getMatches(score => score >= 0.5), getMatches(score => score > 0)];
}) : Promise.reject('assertion failed: LOGfiles miscount')).then(function getTotal(scores) {
if ((scores = scores.filter(Boolean)).length <= 0)
return Promise.reject('all media having too short tracklist,\nmismatching tracklist length, range rip or failed to extract checksums');
const sum = array => array.reduce((sum, val) => sum + val, 0);
const getTotal = index => Math.min(...(index = scores.map(score => score[index]))) > 0 ? sum(index) : 0;
return {
matched: getTotal(1),
partiallyMatched: getTotal(2),
anyMatched: getTotal(3),
total: sum(scores.map(score => score[0])),
};
}))(results.map(result => result && result.entries)) })).length > 0 ? results : Promise.reject('No matches');
});
const methods = [
{ metadata: 'fast', fuzzy: 0 }, { metadata: 'default', fuzzy: 0 }, { metadata: 'extensive', fuzzy: 0 },
//{ metadata: 'fast', fuzzy: 1 }, { metadata: 'default', fuzzy: 1 }, { metadata: 'extensive', fuzzy: 1 },
];
[target.textContent, target.style.color] = ['Looking up...', null];
(function execMethod(index = 0, reason = 'index out of range') {
return index < methods.length ? ctdbLookup(methods[index]).then(results =>
Object.assign(results, { method: methods[index] }),
reason => execMethod(index + 1, reason)) : Promise.reject(reason);
})().then(function(results) {
if (logScoresCache && 'torrentId' in logScoresCache && logScoresCache[torrentId].every(uncalibratedReadOffset))
return Promise.reject('Incorrect read offset');
target.textContent = `${results.length}${Boolean(results.method.fuzzy) ? ' fuzzy' : ''} ${results.method.metadata} ${results.length == 1 ? 'match' : 'matches'}`;
target.style.color = '#' + (['fast', 'default', 'extensive'].indexOf(results.method.metadata) +
results.method.fuzzy * 3 << 1).toString(16) + 'a0';
setTooltip(target, 'Open results in new window');
return queryAjaxAPICached('torrent', { id: torrentId }).then(function(torrent) {
function buildEditionTitle(metadata) {
const editionTitle = [ ];
const countries = metadata.release.filter(release => release.country
&& !['XW'].includes(release.country.toUpperCase())).map(release =>
iso3166ToCountryShort[release.country] || release.country);
if (useCountryInTitle && countries.length > 0 && countries.length < 3)
Array.prototype.push.apply(editionTitle, countries);
return editionTitle.length > 0 ? editionTitle.join(' / ') : undefined;
}
const isCompleteInfo = torrent.torrent.remasterYear > 0
&& Boolean(torrent.torrent.remasterRecordLabel)
&& Boolean(torrent.torrent.remasterCatalogueNumber);
const is = what => !torrent.torrent.remasterYear && ({
unknown: torrent.torrent.remastered,
unconfirmed: !torrent.torrent.remastered,
}[what]);
let [method, confidence] = [results.method, results.confidence];
const confidenceBox = document.createElement('span');
confidence.then(function(confidence) {
if (confidence.anyMatched <= 0) return Promise.reject('mismatch');
let color = confidence.matched || confidence.partiallyMatched || confidence.anyMatched;
color = Math.round(color * 0x55 / confidence.total);
color = 0x55 * (3 - Number(confidence.partiallyMatched > 0) - Number(confidence.matched > 0)) - color;
confidenceBox.innerHTML = svgCheckmark('#' + (color << 16 | 0xCC00).toString(16).padStart(6, '0'));
confidenceBox.className = confidence.matched > 0 ? 'ctdb-verified' : 'ctdb-partially-verified';
setTooltip(confidenceBox, `Checksums${confidence.matched > 0 ? '' : ' partially'} matched (confidence ${confidence.matched || confidence.partiallyMatched || confidence.anyMatched}/${confidence.total})`);
}).catch(function(reason) {
confidenceBox.innerHTML = reason == 'mismatch' ? svgFail() : svgQuestionMark();
confidenceBox.className = 'ctdb-not-verified';
setTooltip(confidenceBox, `Could not verify checksums (${reason})`);
}).then(() => { target.parentNode.append(confidenceBox) });
confidence = confidence.then(confidence =>
is('unknown') && confidence.anyMatched <= 0 ? Promise.reject('mismatch') : confidence,
reason => ({ matched: undefined, partiallyMatched: undefined, anyMatched: undefined }));
if (torrent.torrent.description)
torrentDetails.dataset.torrentDescription = torrent.torrent.description.trim();
// In-page results table
const [thead, table, tbody] = createElements('div', 'table', 'tbody');
thead.style = theadStyle;
thead.innerHTML = `<b>Applicable CTDB matches</b> (method: ${Boolean(method.fuzzy) ? 'fuzzy, ' : ''}${method.metadata})`;
table.style = 'margin: 0; max-height: 10rem; overflow-y: auto; empty-cells: hide;';
table.className = 'ctdb-lookup-results ctdb-lookup-' + torrentId;
const [recordLabels, catalogueNumbers] = editionInfoParser(torrent.torrent);
const labelInfoMapper = metadata => metadata.labelInfo.map(labelInfo => ({
label: labelInfo.name ? rxNoLabel.test(labelInfo.name) ? noLabel :
metadata.source == 'discogs' ? stripDiscogsNameVersion(labelInfo.name) : labelInfo.name : undefined,
catNo: rxNoCatno.test(labelInfo.catno) ? undefined : labelInfo.catno,
})).filter(labelInfo => labelInfo.label || labelInfo.catNo);
const _getReleaseYear = metadata => (metadata = metadata.release.map(release => getReleaseYear(release.date)))
.every((year, ndx, arr) => year > 0 && year == arr[0]) ? metadata[0] : NaN;
results.forEach(function(metadata) {
function applyTooltip() {
if (metadata.extra.length > 0 || metadata.infourl)
(tr.title ? title.querySelector('a.' + metadata.source + '-release') : tr).title =
metadata.extra.concat(metadata.infourl).filter(Boolean).join('\n\n');
}
const [tr, source, artist, title, release, editionInfo, barcode, relevance] =
createElements('tr', 'td', 'td', 'td', 'td', 'td', 'td', 'td', 'td');
[tr.className, tr.style] = ['ctdb-metadata', 'transition: color 200ms ease-in-out;'];
tr.dataset.releaseUrl = [({
musicbrainz: mbOrigin,
discogs: dcOrigin,
}[metadata.source]), 'release', metadata.id].join('/');
[release, barcode, relevance].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[relevance].forEach(elem => { elem.style.textAlign = 'right' });
if (source.innerHTML = GM_getResourceText(({ musicbrainz: 'mb_logo', discogs: 'dc_icon' }[metadata.source]))) {
source.firstElementChild.removeAttribute('width');
source.firstElementChild.setAttribute('height', '1em');
svgSetTitle(source.firstElementChild, metadata.source);
} else source.innerHTML = `<img src="http://s3.cuetools.net/icons/${metadata.source}.png" height="12" title="${metadata.source}" />`;
artist.textContent = metadata.source == 'discogs' ? stripDiscogsNameVersion(metadata.artist) : metadata.artist;
source.style.alignTop = '1pt';
title.innerHTML = linkHTML(tr.dataset.releaseUrl, metadata.album, metadata.source + '-release');
if (metadata.source == 'discogs') findDiscogsRelatives('release', metadata.id).then(function(releases) {
title.style = 'display: inline-flex; flex-flow: row wrap; column-gap: 3pt;';
const span = Object.assign(document.createElement('span'), { className: 'musicbrainz-relations' });
span.style = 'display: flex; flex-flow: column wrap; max-height: 1em;';
span.append.apply(span, releases.map((release, index) => Object.assign(document.createElement('a'), {
href: [mbOrigin, 'release', release.id].join('/'), target: '_blank',
style: noLinkDecoration + ' vertical-align: top;',
innerHTML: '<img src="https://musicbrainz.org/static/images/entity/release.svg" height="6" />',
title: release.id,
})));
title.append(span);
});
if (Array.isArray(metadata.release)) fillListRows(release, Array.prototype.concat.apply([ ],
metadata.release.map(release => iso3166ToFlagCodes([release.country]).map(countryCode =>
releaseEventMapper(countryCode, release.date, torrent.torrent.remasterYear)))), 3);
if (Array.isArray(metadata.labelInfo)) fillListRows(editionInfo, metadata.labelInfo.map(labelInfo =>
editionInfoMapper(stripDiscogsNameVersion(labelInfo.name), labelInfo.catno, recordLabels, catalogueNumbers)));
if (editionInfo.childElementCount <= 0 && metadata.source == 'musicbrainz')
mbFindEditionInfoInAnnotation(editionInfo, metadata.id);
if (metadata.barcode) {
barcode.textContent = metadata.barcode;
if (catalogueNumbers.some(catalogueNumber => sameBarcodes(catalogueNumber, metadata.barcode)))
editionInfoMatchingStyle(barcode);
}
if (metadata.relevance >= 0) [relevance.textContent, relevance.title] =
[metadata.relevance + '%', 'Relevance'];
const releaseYear = _getReleaseYear(metadata);
const _editionInfo = labelInfoMapper(metadata);
if (!_editionInfo.some(labelInfo => labelInfo.catNo) && metadata.barcode)
_editionInfo.push({ catNo: metadata.barcode });
if (releaseYear > 0 && _editionInfo.length > 0) {
tr.dataset.remasterYear = releaseYear;
setEditionInfo(tr, _editionInfo);
const editionTitle = buildEditionTitle(metadata);
if (editionTitle) tr.dataset.remasterTitle = editionTitle;
(!isCompleteInfo && 'editionGroup' in torrentDetails.dataset && !Boolean(method.fuzzy)
&& !noEditPerms && (editableHosts.includes(document.domain) || ajaxApiKey)
&& (!is('unknown') || method.metadata != 'extensive' || !(metadata.relevance < 100)) ?
confidence : Promise.reject('Not applicable')).then(function(confidence) {
if (!(releaseYear > 0)) throw 'Unknown or inconsistent release year';
if (_editionInfo.length <= 0 && torrent.torrent.remasterYear > 0)
throw 'No additional edition information';
applyOnClick(tr);
}).catch(reason => { applyOnCtrlClick(tr) }).then(applyTooltip);
} else applyTooltip();
tr.append(source, artist, title, release, editionInfo, barcode, relevance);
for (let cell of tr.cells) cell.style.backgroundColor = 'inherit';
['source', 'artist', 'title', 'release-events', 'edition-info', 'barcode', 'relevance'].forEach(function(className, index) {
tr.cells[index].style.backgroundColor = 'inherit';
tr.cells[index].className = className;
});
tbody.append(tr);
});
table.append(tbody);
addResultsFilter(thead, tbody, 5);
addLookupResults(torrentId, thead, table);
// Group set
if (isCompleteInfo || !('editionGroup' in torrentDetails.dataset) || Boolean(method.fuzzy)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey
|| torrent.torrent.remasterYear > 0 && !(results = results.filter(metadata =>
isNaN(metadata = _getReleaseYear(metadata)) || metadata == torrent.torrent.remasterYear))
.some(metadata => metadata.labelInfo && metadata.labelInfo.length > 0 || metadata.barcode)
|| results.length > (is('unknown') ? 1 : 2)
|| is('unknown') && method.metadata == 'extensive' && results.some(metadata => metadata.relevance < 100))
return;
confidence.then(function(confidence) {
const releaseYear = results.reduce((year, metadata) => isNaN(year) ? NaN :
(metadata = _getReleaseYear(metadata)) > 0 && (year <= 0 || metadata == year) ? metadata : NaN, -Infinity);
if (!(releaseYear > 0) || !results.every(m1 => m1.release.every(r1 => results.every(m2 =>
m2.release.every(r2 => getReleaseYear(r2.date) == getReleaseYear(r1.date))))))
throw 'No additional edition information';
const a = document.createElement('a');
[a.className, a.href, a.textContent] = ['update-edition', '#', '(set)'];
if (results.length > 1 || results.some(result => result.relevance < 100)
|| !(confidence.partiallyMatched > 0)) {
a.style.fontWeight = 300;
a.dataset.confirm = true;
} else a.style.fontWeight = 'bold';
let editionInfo = Array.prototype.concat.apply([ ], results.map(labelInfoMapper));
const barcodes = results.map(metadata => metadata.barcode).filter(Boolean);
if (!editionInfo.some(labelInfo => labelInfo.catNo) && barcodes.length > 0)
Array.prototype.push.apply(editionInfo, barcodes.map(barcode => ({ catNo: barcode })));
if (editionInfo.length <= 0 && torrent.torrent.remasterYear > 0)
throw 'No additional edition information';
a.dataset.remasterYear = releaseYear;
setEditionInfo(a, editionInfo);
if (results.length < 2) {
a.dataset.releaseUrl = ({
musicbrainz: [mbOrigin, 'release', results[0].id],
discogs: [dcOrigin, 'release', results[0].id],
}[results[0].source]).join('/');
const editionTitle = buildEditionTitle(results[0]);
if (editionTitle) tr.dataset.remasterTitle = editionTitle;
}
setTooltip(a, 'Update edition info from matched release(s)\n\n' + results.map(metadata =>
metadata.labelInfo.map(labelInfo => (({ discogs: 'Discogs', musicbrainz: 'MusicBrainz' }[metadata.source])) + ' ' + [
_getReleaseYear(metadata),
[stripDiscogsNameVersion(labelInfo.name), labelInfo.catno || metadata.barcode].filter(Boolean).join(' / '),
].filter(Boolean).join(' – ') + (metadata.relevance >= 0 ? ` (${metadata.relevance}%)` : ''))
.filter(Boolean).join('\n')).join('\n'));
a.onclick = updateEdition;
target.parentNode.append(a);
});
}, alert);
}, function(reason) {
target.textContent = reason;
target.style.color = 'red';
setTooltip(target);
}).then(() => { target.dataset.haveQuery = true });
}, 'Lookup edition in CUETools DB (TOCID)');
}
const elem = document.body.querySelector('div#discog_table > div.box.center > a:last-of-type');
if (elem != null) {
const a = document.createElement('a'), captions = ['Incomplete editions only', 'All editions'];
[a.textContent, a.href, a.className, a.style.marginLeft] = [captions[0], '#', 'brackets', '2rem'];
a.onclick = function(evt) {
if (captions.indexOf(evt.currentTarget.textContent) == 0) {
for (let strong of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.edition.discog > td.edition_info > strong')) (function(tr, show = true) {
if (show) (function(tr) {
show = false;
while ((tr = tr.nextElementSibling) != null && tr.classList.contains('torrent_row')) {
const a = tr.querySelector('td > a:last-of-type');
if (a == null || !/\bFLAC\s*\/\s*Lossless\s*\/\s*Log\s*\(\-?\d+%\)/.test(a.textContent)) continue;
show = true;
break;
}
})(tr);
if (show) (function(tr) {
while (tr != null && !tr.classList.contains('group')) tr = tr.previousElementSibling;
if (tr != null && (tr = tr.querySelector('div > a.show_torrents_link')) != null
&& tr.parentNode.classList.contains('show_torrents')) tr.click();
})(tr); else (function(tr) {
do tr.hidden = true;
while ((tr = tr.nextElementSibling) != null && tr.classList.contains('torrent_row'));
})(tr);
})(strong.parentNode.parentNode, incompleteEdition.test(strong.lastChild.textContent.trim()));
for (let tr of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.group.discog')) (function(tr) {
if (!(function(tr) {
while ((tr = tr.nextElementSibling) != null && !tr.classList.contains('group'))
if (tr.classList.contains('edition') && !tr.hidden) return true;
return false;
})(tr)) tr.hidden = true;
})(tr);
} else for (let tr of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.discog'))
tr.hidden = false;
evt.currentTarget.textContent = captions[1 - captions.indexOf(evt.currentTarget.textContent)];
};
elem.after(a);
}