// ==UserScript==
// @name [GMT] Edition lookup by CD TOC
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.16.8
// @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
// @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
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/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, mbRequestsCache = new Map;
let sessionsSessionCache, mbLastRequest = null, 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]+/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 mbRequestRate = 1000;
function mbApiRequest(endPoint, params) {
if (!endPoint) throw 'Endpoint is missing';
const url = new URL('/ws/2/' + endPoint.replace(/^\/+|\/+$/g, ''), mbOrigin);
url.search = new URLSearchParams(Object.assign({ fmt: 'json' }, params));
const cacheKey = url.pathname.slice(6) + url.search;
if (mbRequestsCache.has(cacheKey)) return mbRequestsCache.get(cacheKey);
const request = new Promise((resolve, reject) => { (function request(reqCounter = 1) {
if (reqCounter > 60) return reject('Request retry limit exceeded');
if (mbLastRequest == Infinity) return setTimeout(request, 50, reqCounter);
const now = Date.now();
if (now <= mbLastRequest + mbRequestRate)
return setTimeout(request, mbLastRequest + mbRequestRate - now, reqCounter);
mbLastRequest = Infinity;
globalXHR(url, { responseType: 'json' }).then(function({response}) {
mbLastRequest = Date.now();
resolve(response);
}, function(reason) {
mbLastRequest = Date.now();
reject(reason);
});
})() });
mbRequestsCache.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;
}
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;
}
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 null;
}))));
}
class DiscID {
constructor() { this.id = '' }
addValues(values, width = 0, length = 0) {
if (!Array.isArray(values)) values = [values];
values = values.map(value => value.toString(16).toUpperCase().padStart(width, '0')).join('');
this.id += width > 0 && length > 0 ? values.padEnd(length * width, '0') : values;
return this;
}
toDigest() {
return CryptoJS.SHA1(this.id).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().addValues(mbTOC.slice(0, 2), 2).addValues(mbTOC.slice(2), 8, 100).toDigest();
}
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|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;
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 url = 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 => { group.disabled = !evt.currentTarget.value },
selectionStart: 0, selectionDirection: 'backward',
}), group = Object.assign(document.createElement('fieldset'), {
style: 'margin-top: 1em; box-sizing: border-box; padding: 5pt; display: flex; flex-flow: column; row-gap: 4pt; border: 2pt groove #ddd;',
disabled: !Boolean(input),
}), 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' ? url.value : null);
},
}), form = Object.assign(document.createElement('form'), {
method: 'dialog',
style: 'width: 35em; font: 9pt "Noto Sans", sans-serif;',
onsubmit: function(evt) {
evt.currentTarget.querySelectorAll('input[type="checkbox"]')
.forEach(input => { if (input.option) input.option[1] = input.checked });
},
});
form.append(Object.assign(document.createElement('div'), {
textContent: title,
style: 'font-weight: bold; font-size: medium; color: coral; margin-bottom: 1em;',
}));
if (prompt) {
const label = Object.assign(document.createElement('label'), {
textContent: prompt,
style: 'white-space: pre-line;',
});
url.style.marginTop = '5pt';
label.append(url);
form.append(label);
} else form.append(url);
if (options) for (let option of options) if (option[0]) {
const label = Object.assign(document.createElement('label'), {
style: 'display: block;',
}), input = Object.assign(document.createElement('input'), {
type: 'checkbox',
checked: Boolean(option[1]),
style: 'margin-right: 5pt;',
option: option,
});
if (option[2]) label.title = option[2];
label.append(input, option[0]);
group.append(label);
}
if (group.childElementCount > 0) form.append(group);
const buttons = document.createElement('div');
buttons.style = 'margin-top: 15pt; display: flex; flex-flow: row; justify-content: flex-end; column-gap: 5pt;';
buttons.append(Object.assign(document.createElement('input'), {
type: 'submit',
value: 'OK',
style: 'flex-basis: 5em; cursor: pointer; font: 9pt "Noto Sans", sans-serif;',
}), Object.assign(document.createElement('input'), {
type: 'button',
value: 'Cancel',
style: 'flex-basis: 5em; cursor: pointer; font: 9pt "Noto Sans", sans-serif;',
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;
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 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)) 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))).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 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 = evt => { 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 && ![mbidVA].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 = artistCredit.anv || stripDiscogsNameVersion(artistCredit.name);
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] = [['closest editions only', 'show all'], 'filtered'];
thead.append(Object.assign(document.createElement('span'), {
style: 'float: right; color: cadetblue; cursor: pointer;',
className: 'filter-switch',
textContent: '[' + labels[0] + ']',
onclick: function(evt) {
const filtered = evt.currentTarget.classList.contains(cls);
for (let tr of tbody.rows) tr.hidden = !filtered && !filteredRows.includes(tr);
evt.currentTarget.textContent = '[' + labels[filtered ? 0 : 1] + ']';
evt.currentTarget.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>`;
const mbidVA = '89ad4ac3-39f7-470e-963a-56509c546377';
const mbidUnknown = '125ec42a-7229-4250-afc5-e057484327fe';
const stripDiscogsNameVersion = name => name && name.replace(/\s+\(\d+\)$/, '');
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 path of target.getElementsByTagName('path')) path.removeAttribute('fill');
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) {
function openInNewWindow(background = false) {
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, background);
}
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) openInNewWindow(!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.style, thead.style.minHeight] =
['edition-search-results', 6, 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');
}
}
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>`;
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());
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 getGID({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');
}
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);
const params = { inc: [
'release-groups', 'artist-credits', 'labels', 'recordings',
'artist-rels', 'label-rels', 'series-rels', 'place-rels', 'work-rels', 'url-rels', 'release-rels',
].join('+') };
if (!mbDiscID || allowTOCLookup) params.toc = mbTOC.join(' ');
if (anyMedia) params['media-format'] = 'all';
return mbApiRequest('discid/' + (mbDiscID || '-'), params).then(function(result) {
if (!Array.isArray(result.releases) || result.releases.length <= 0)
return Promise.reject('MusicBrainz: no matches');
console.log('MusicBrainz lookup by discId/TOC successfull:', mbDiscID, '/', params, 'releases:', result.releases);
console.assert(!result.id || result.id == mbDiscID, 'mbLookupByDiscID ids mismatch', result.id, mbDiscID);
return { mbDiscID: mbDiscID, mbTOC: mbTOC, attached: Boolean(result.id), releases: result.releases };
});
}
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[(entityType + 's').replace(/s(?=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 sameTitleMapper(entry, title, cmpFn = sameStringValues, normFn = str => str.trim()) {
const compareTo = root => (root.title || root.name) && cmpFn(normFn(root.title || root.name), normFn(title));
return entry && title && (compareTo(entry) || entry.aliases && entry.aliases.some(compareTo));
}
function trackTitleNorm(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 = {
'Keyboards': 'Keyboard',
'Synth': 'Synthesizer',
'Drum Programming': 'Drums Programming',
}[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;
return mbApiRequest('instrument', {
query: `instrument:"${_creditType}" OR alias:"${_creditType}"`,
}).then(results => results.instruments).then(function(instruments) {
if (debugLogging) console.debug('Instruments found for "%s":', creditType, instruments);
if (instruments.length > 1) {
const strictly = instruments.filter(instrument => similarStringValues(instrument.name, _creditType));
instruments = strictly.length > 0 ? strictly
: instruments.filter(instrument => sameTitleMapper(instrument, _creditType, similarStringValues));
}
if (instruments.length <= 0) {
let allInstruments = GM_getValue('instruments');
allInstruments = 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;
});
return allInstruments.then(allInstruments => allInstruments.includes(creditType) ? [{
id: '0a06dd9a-92d6-4891-a699-2b116a3d3f37', // other instruments
creditedAs: creditedAs.replace(...untitleCase),
}] : Promise.reject(`Resolved as non instrument (${creditType})`));
} else if (instruments.length > 1) console.warn('Ambiguous instrument binding for %s:', creditType, instruments);
return [{ id: instruments[0].id, creditedAs: creditedAs != _creditType ? creditedAs.replace(...untitleCase) : undefined }];
});
}
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) {
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.torrent.remasterTitle)
formData.set('comment', decodeHTML(torrent.torrent.remasterTitle)/*.toLowerCase()*/);
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', mbidVA);
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);
if (torrent.torrent.remasterRecordLabel) {
const label = decodeHTML(torrent.torrent.remasterRecordLabel);
if (rxNoLabel.test(label)) formData.set('labels.0.mbid', '157afde4-4bf5-4039-8ad2-5a15acc85176');
else formData.set('labels.0.name', label);
}
if (torrent.torrent.remasterCatalogueNumber) {
const catNo = decodeHTML(torrent.torrent.remasterCatalogueNumber);
formData.set('labels.0.catalog_number', rxNoCatno.test(catNo) ? '[none]' : catNo);
let barcode = catNo.split(' / ').map(catNo => catNo.replace(/\W+/g, ''));
if (barcode = barcode.find(RegExp.prototype.test.bind(/^\d{10,13}$/))) formData.set('barcode', barcode);
}
if (GM_getValue('insert_torrent_reference', false)) formData.set('edit_note', ((formData.get('edit_note') || '') + `
Seeded from torrent ${document.location.origin}/torrents.php?torrentid=${torrent.torrent.id} edition info`).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)) throw 'Invalid argument';
params = Object.assign({
tracklist: true, groupTracks: true, alignByTOCs: false,
mbidLookup: true, recordingsLookup: true, lookupArtistsByRecording: true, rgLookup: true,
searchSize: GM_getValue('mbid_search_size', 30),
languageIdentifier: true,
composeAnnotation: GM_getValue('compose_annotation', true),
openInconsistent: GM_getValue('open_inconsistent', true),
assignUncertain: GM_getValue('assign_uncertain', false),
createEntries: GM_getValue('mb_create_entries', 2),
createUncertainArtists: GM_getValue('create_uncertain_artists', false),
extendedMetadata: false, rgRelations: false, releaseRelations: false,
recordingRelations: false, workRelations: false, preferTrackRelations: false,
}, params);
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, contexts: [context] };
if (entry.anv) lookupIndexes[entity][entry.id].anv = entry.anv;
} else 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 ('anv' in entry) _entry.anv = entry.anv;
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(roots, roleTrackEvaluator) {
const extraArtists = { };
console.assert(Array.isArray(roots) && roots.length > 0, roots);
console.assert(typeof roleTrackEvaluator == 'function', roleTrackEvaluator);
if (!roots.includes(release)) roots.unshift(release);
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) {
if (!roots.includes(release)) roots.unshift(release);
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, role => !findRelationLevels('artist', role).some(isReleaseLevel)),
};
}
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(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 (artist.anv) formData.set(`${prefix}.name`, capitalizeName(artist.anv));
else formData.delete(`${prefix}.name`);
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.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 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 romanToArabic(input) {
const romans = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
return [...input.trim().toUpperCase()].reduce((previous, current, index, array) =>
romans[array[index + 1]] > romans[current] ? previous - romans[current] : previous + romans[current], 0);
}
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(root, titles) {
if (Array.isArray(root)) root.forEach(function(track) {
if (track.type_ == 'index' && !collapseSubtracks)
return addTracks(track.sub_tracks, (titles || [ ]).concat(track.title));
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 || !titles && 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(function(subTrack) {
const trackArtists = resolveArtists(track, subTrack);
return !sameTrackArtists(trackArtists) ? trackArtists : 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, titles: titles, length: duration,
artists: track.artists, extraartists: track.extraartists, trackArtists: trackArtists,
});
});
})(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(function(track) {
const trackArtists = resolveArtists(track);
return !sameTrackArtists(trackArtists) ? trackArtists : 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, titles: tracks[trackNo][0].titles,
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) {
if (!discogsBindingsCache) {
const timeStamp = Date.now();
if (!(discogsBindingsCache = GM_getValue('discogs_to_mb_bindings'))) discogsBindingsCache = {
artist: {
194: mbidVA,
598667: '9be7f096-97ec-4615-8957-8d40b5dcbc41', // trad.
451329: null,
}, // VA
label: {
750: '49b58bdb-3d74-40c6-956a-4c4b46115c9c', // Virgin
895: '1ca5ed29-e00b-4ea5-b817-0bcca0e04946', // RCA
1003: null, // BMG ambiguous
1818: '157afde4-4bf5-4039-8ad2-5a15acc85176', // no label
1866: '011d1192-6f65-45bd-85c4-0400dd45693e', // Columbia
2345: '3730c0ea-3dc2-45c3-ac5c-9d482921ea51', // Warner
5320: 'f18f3b31-8263-4de3-966a-fda317492d3d', // Decca
5870: null, // Metronome ambiguous
26126: 'c029628b-6633-439e-bcee-ed02e8a338f7', // EMI
51167: null, // Rough Trade ambiguous
108701: '7c439400-a83c-48bc-9042-2041711c9599', // Virgin JP
},
series: {
77074: '713c4a95-6616-442b-9cf6-14e1ddfd5946', // Blue Note Records => Blue Note
},
}; 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 });
}
if (!(entity in discogsBindingsCache)) discogsBindingsCache[entity] = { };
if (!(discogsId in discogsBindingsCache[entity])) return Promise.reject('Not cached');
if (!rxMBID.test(discogsBindingsCache[entity][discogsId])) return Promise.resolve(null);
return globalXHR(`${mbOrigin}/${entity}/${discogsBindingsCache[entity][discogsId]}`, {
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 != discogsBindingsCache[entity][discogsId]) saveToCache(entity, discogsId, response); //return Promise.reject('Location changed');
return response;
});
}
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 query = { }, name = stripDiscogsNameVersion(lookupIndexes[entity][discogsId].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;
}
}
if (lookupIndexes[entity][discogsId].anv && lookupIndexes[entity][discogsId].anv != name)
addField('alias', '"' + lookupIndexes[entity][discogsId].anv + '"');
return Object.keys(query).map(field => query[field].map(expr => `${field}:(${expr})`).join(' OR ')).join(' OR ');
}
function findMBIDByCommonTitles(entity, discogsId, mbids) {
function getReleases(mbid, params) {
const safeErrorHandler = reason => (console.warn(reason), null);
const workers = [mbApiRequest(entity + '/' + mbid, { inc: 'aliases+release-rels+release-group-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({
type: relation['target-type'],
relationType: relation.type,
}, relation[relation['target-type']])).filter((r1, n, a) =>
a.findIndex(r2 => ['type', 'id'].every(prop => r2[prop] == r1[prop])) == n);
}).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({ type: '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 || !(discogsId > 0) || !Array.isArray(mbids)) return Promise.reject('Invalid argument');
if (mbids.length <= 0) return Promise.reject('No MusicBrainz entries');
const getDiscogsReleases = (page = 1) => dcApiRequest(`${discogsEntity(entity)}s/${discogsId}/releases`, {
page: page,
per_page: 500,
}).then(function(response) {
if (debugLogging && response.pagination.page > 1 && response.pagination.page % 50 == 0)
console.info('getDiscogsReleases page %d/%d', response.pagination.page, response.pagination.pages);
return response.pagination.page < response.pagination.pages ?
getDiscogsReleases(response.pagination.page + 1)
.then(releases => response.releases.concat(releases)) : response.releases;
});
const dcReleasesWorker = getDiscogsReleases();
const lookupMethods = [{
worker: {
artist: mbid => getReleases(mbid, ['artist', 'track_artist']),
label: mbid => getReleases(mbid, [entity]),
series: mbid => getReleases(mbid),
place: mbid => getReleases(mbid),
}[entity],
resolver: function(dcReleases, results) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'), true);
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/'), true);
}
const mutualScores = results.map(results => results ? results.reduce(function(score, result) {
let releases = Array.prototype.concat.apply([ ], getDiscogsRels(result).map(discogsId =>
dcReleases.filter(dcRelease => dcRelease.type == 'release' && dcRelease.id == discogsId)));
if (result['release-group']) Array.prototype.push.apply(releases,
getDiscogsRels(result['release-group'], 'master').map(discogsId =>
dcReleases.filter(dcRelease => dcRelease.type == 'master' && dcRelease.id == discogsId)));
if (releases.length <= 0 && (releases = dcReleases.filter(function(dcRelease) {
if (!(dcRelease.year > 0)) return false;
const releaseYear = result.type == 'release' ? getReleaseYear(result.date) : NaN;
const rgYear = result.type == 'release' && result['release-group']
&& getReleaseYear(result['release-group']['first-release-date'])
|| result.type == 'release_group' && getReleaseYear(result['first-release-date']) || NaN;
const strongYearMatch = dcRelease.type == 'release' && dcRelease.year == releaseYear
|| dcRelease.type == 'master' && dcRelease.year == rgYear;
const weakYearMatch = dcRelease.type == 'release' && dcRelease.year >= rgYear
|| dcRelease.type == 'master' && dcRelease.year <= rgYear;
return strongYearMatch && sameTitleMapper(result, dcRelease.title, sameStringValues, releaseTitleNorm)
|| weakYearMatch && sameTitleMapper(result, dcRelease.title, sameStringValues);
})).length <= 0) return score;
if (debugLogging) console.debug('Found matching releases:', result, releases);
return score + (result.relationType == 'track_artist'
|| releases.every(release => release.role == 'TrackAppearance') ? 1/2 : 1);
}, 0) : 0);
const 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);
if (!(hiScore >= 1)) if (params.assignUncertain) openUncertain();
else return Promise.reject('Not found by common releases');
const dataSize = Math.min(dcReleases.length, results[hiIndex].length);
if (!(hiScore * 50 >= dataSize)) if (!params.assignUncertain)
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:\n%s\n%s',
hiScore, [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 (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (releases)');
beep.play();
if (params.openInconsistent) openInconsistent(entity, discogsId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), '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];
},
}, {
worker: { artist: getArtistRecordings }[entity],
resolver: function(dcReleases, recordings) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'recordings'].join('/'), true);
GM_openInTab([dcOrigin, discogsEntity(entity), discogsId].join('/'), true);
}
dcReleases = dcReleases.filter(dcRelease => dcRelease.trackinfo);
const mutualScores = recordings.map(recordings => recordings ? recordings.reduce(function(score, recording) {
if (!recording) return score;
let releases = dcReleases.filter(dcRelease =>
sameTitleMapper(recording, dcRelease.trackinfo, sameStringValues, title => title && [
/\s+\((?:(?:feat(?:\b|\.|uring)|ft\.).+|.+\b(?:re)?mix|live|(?:en|ao) (vivo|directo?))\)$/i,
].reduce((str, rx) => str.replace(rx, ''), title.trim())));
if (releases.length <= 0) return score;
if (debugLogging) console.debug('Found matching releases:', recording, releases);
return score + 0.5 + (releases.length - 1) * 0.25;
}, 0) : 0);
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 recordings');
const hiIndex = mutualScores.indexOf(hiScore);
if (!(hiScore >= 1)) if (params.assignUncertain) openUncertain();
else return Promise.reject('Not found by common recordings');
const dataSize = Math.min(dcReleases.length, recordings[hiIndex].length);
if (!(hiScore * 50 >= dataSize)) if (!params.assignUncertain)
return Promise.reject('Matched by common recordings with too low score');
else if (hiScore >= 1) openUncertain();
console.log('Entity binding found by having score %f:\n%s\n%s',
hiScore, [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));
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (recordings)');
beep.play();
if (params.openInconsistent) openInconsistent(entity, discogsId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'recordings');
}
discogsName(entity, discogsId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${dataSize} recording(s)`, 'gold') });
return mbids[hiIndex];
},
}];
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)));
}
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 }
return entry.relations.filter(relation => relation['target-type'] == 'url').some(function(relation) {
try { relation = new URL(relation.url.resource) } catch(e) { return false }
return relation.hostname + relation.pathname + relation.search
== url.hostname + url.pathname + url.search;
});
})) {
if (debugLogging) console.debug('Same %ss found by having same url(s):',
entity, discogsEntry, entry);
return entry.id;
}
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;
}
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 > 0);
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)
|| entity == 'label' && equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name),
label => label && label.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)) });
}
if (!params.mbidLookup) params.searchSize = 0;
if (params.recordingRelations || params.workRelations) params.tracklist = true;
const rxMLang = /^(.+?)\s*=\s*(.+)$/, literals = { }, lookupIndexes = { };
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 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();
formData.set('name', normTitle(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');
}
});
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, 'Concept By': 162, 'Concept-By': 162,
'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,
'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,
'Legal': 142, 'Booking': 134, 'Creative Director': 146, 'Engineer': 138, 'Engineer [Audio]': 140,
'Producer': 141, 'Produced By': 141,'Produced-By': 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,
'Other': 129, 'Sequenced By': 129, 'Sequenced-By': 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,
//'Mastered By': 136, 'Graphic Design': 125, 'Design': 130, 'Illustration': 130, 'Photography': 123, 'Photography By': 123,
},
'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,
'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,
'Engineer [Editor]': 38, 'Engineer [Balance]': 727, 'Balance Engineer': 727, 'Engineer [Transfer]': 1179, 'Transfer Engineer': 1179,
'Music Director': 1187, 'Audio 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, 'Booklet Editor': 929,
'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,
'Text-By': 25, 'Administrator': 25, 'Advisor': 25, 'Booking': 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, 'Sequenced By': 25, 'Sequenced-By': 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,
},
'release-group': { 'A&R': 62, 'Creative Director': 63 },
},
'label': {
'release': {
'Copyright (c)': 708, 'Phonographic Copyright (p)': 711, 'Licensed To': 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 isTrackLevel = level => ['recording', 'work'].includes(level);
const isReleaseLevel = level => ['release-group', 'release'].includes(level);
const relateAtLevel = sourceEntity => sourceEntity && ({
'work': params.workRelations,
'recording': params.recordingRelations,
'release': params.releaseRelations,
'release-group': params.rgRelations,
}[sourceEntity]);
const taskAttribute = task => task ? {
id: '39867b3b-0f1e-40d5-b602-4f3936b7f486',
value: task.replace(...untitleCase),
} : null;
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 roleModifiers = ['Soloist', 'Guest'];
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) for (let extraArtist of release.extraartists) {
const roles = getRoles(extraArtist), modifiers = [ ];
for (let role of roleModifiers) if (roles.includes(role)) modifiers.push(role);
for (let role of roles) if (!roleModifiers.includes(role)) addCredit('artist', role, extraArtist, modifiers);
}
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');
if (release.companies) for (let company of release.companies) for (let entity of ['label', 'place'])
if (findRelationLevels(entity, company.entity_type_name).length > 0)
addCredit(entity, company.entity_type_name, company);
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) {
const name = stripDiscogsNameVersion(label.name);
if (rxNoLabel.test(name)) formData.set(prefix + '.mbid', '157afde4-4bf5-4039-8ad2-5a15acc85176');
else {
formData.set(prefix + '.name', capitalizeName(name));
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+)$/];
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.alignByTOCs) mediaSplitters.push(function alignByTOCs(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, ndx, tracks) => track.heading == tracks[0].heading)) {
medium.name = medium.tracks[0].heading;
for (let track of medium.tracks) track.heading = undefined;
}
(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`, normTitle(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) {
const modifiers = [ ];
for (let role of roleModifiers) if (extraArtist.roles.includes(role)) modifiers.push(role);
for (let role of extraArtist.roles) if (!roleModifiers.includes(role)) {
const levels = findRelationLevels('artist', role);
if (params.recordingRelations && (levels.length <= 0 || levels.includes('recording'))
|| params.workRelations && levels.includes('work'))
addCredit('artist', role, extraArtist, modifiers);
}
}
}
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 = trackTitleNorm(multilingual ? track.name.replace(rxMLang, '$1') : track.name, formData);
formData.set(prefix + 'name', normTitle(_prefix(track.heading) +
_prefix((track.titles || [ ]).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(track);
if (!sameTrackArtists(trackArtist)) seedArtists(trackArtist, prefix);
addArtistCredits(trackArtist);
}
if (track.length) formData.set(prefix + 'length', track.length);
});
});
}
if ('artist' in credits) for (let role in credits.artist) if (!['Featuring'].includes(role)) {
const levels = findRelationLevels('artist', role);
if (levels.length <= 0 && !(role in relationResolvers)) {
const resolvers = [ ], roleParser = /^(.+)\s+\[([^\[\]]+)\]$/.exec(role);
if (roleParser != null) resolvers.push(function() {
if (['Instruments', 'Musician'].includes(roleParser[1])) return instrumentResolver(roleParser[2])
.catch(reason => null).then(attributes => instrumentMapper(attributes, roleParser[2]));
const levels = findRelationLevels('artist', roleParser[1]);
if (levels.length <= 0) return params.recordingRelations ?
instrumentResolver(roleParser[1]).then(attributes => instrumentMapper(attributes, roleParser[1], roleParser[2]))
: Promise.reject(role + ' not resolved');
if (/\b(?:Band|Orchestra|Ensemble)$/.test(roleParser[1])) return Promise.reject(role + ' not resolved');
return instrumentResolver(roleParser[2]).catch(reason => null).then(instrument => levels.map(function(level) {
function testForAttribute(expr, appliesTo, attributeId) {
if (!attributeId) throw 'Invalid argument';
if ((!expr || expr.test(roleParser[2]))
&& (!Array.isArray(appliesTo) || appliesTo.includes(relation.linkTypeId)))
var attribute = { id: attributeId };
if (attribute) relation.attributes = [attribute];
return attribute || null;
}
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][roleParser[1]] };
console.assert(relation.linkTypeId > 0, level, role);
if ([25, 129, 162].includes(relation.linkTypeId)) creditType = roleParser[2]; else {
relation.creditType = roleParser[1];
if (instrument && instrumentRelIds.includes(relation.linkTypeId)) relation.attributes = 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|Featuring)\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$|ed[ \-]By\b))/, [24], '25dfb08e-9b99-44db-b30c-1d6ec6747af8')
&& [20, 30, 62, 138, 141, 143, 993].includes(relation.linkTypeId))
relation.attributes = [taskAttribute(roleParser[2])];
}
return relation;
}));
}); else resolvers.push(() => instrumentResolver(role).then(instrumentMapper));
for (let resolver of resolvers) if (!relationResolvers[role]) relationResolvers[role] = resolver();
else relationResolvers[role] = relationResolvers[role].catch(resolver);
}
if (levels.length > 0 ? levels.some(relateAtLevel) : role in relationResolvers)
for (let extraArtist of credits.artist[role]) addLookupEntry('artist', extraArtist, role);
}
for (let entity of ['label', 'place']) if (entity in credits) for (let typeName in credits[entity]) {
const levels = findRelationLevels(entity, typeName);
if (levels.length > 0 && levels.some(relateAtLevel)) for (let company of credits[entity][typeName])
if (typeName != 'Record Company' || !release?.labels.some(label => label.id == company.id))
addLookupEntry(entity, company, typeName);
}
if (release.series) for (let series of release.series) addLookupEntry('series', series, 'Part Of');
if (!media && release.tracklist) release.tracklist.forEach(track =>
{ 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/.source]: 'digibook',
[/Digipac?k(?: (?:Case|Cover))?/.source]: 'digipak',
[/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 'CD-TEXT': return false;
}
return descriptor.replace(...untitleCase).trim();
}).filter(Boolean);
//else formData.delete('comment');
if (params.composeAnnotation) {
let annotation = [
!params.releaseRelations && !params.rgRelations && 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 (params.rgRelations && 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 (descriptors.length > 0) formData.set('comment', descriptors.join(', '));
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from Discogs release id ${release.id}`).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) && track.length) {
if (/^(\d+)$/.test(track.length)) trackLength = parseInt(track.length);
else if ((trackLength = /^(\d+):(\d+)$/.exec(track.length)) != null)
trackLength = trackLength.slice(1).reverse()
.reduce((s, t, n) => s + parseInt(t) * 60**n, 0) * 1000;
console.assert(trackLength > 0, track.length);
}
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:"' + (artist.anv || stripDiscogsNameVersion(artist.name)) + '")';
});
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) return false;
if (!sameStringValues(recording.title, track.name) && ['(?:re-?)?mix(?:ed)?|RMX'].some(function(rx) {
rx = new RegExp('\\s+\\(.*\\b(?:' + rx + ')\\b.*\\)$', 'i');
let recordingFlag = rx.test(recording.title), trackFlag = rx.test(track.name);
return recordingFlag != trackFlag;
})) return false;
if (recording.releases && ['Live', 'Interview', 'Demo'].some(function(secondaryType) {
const hasSecondaryType = release => 'release-group' in release
&& 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType);
return hasType(secondaryType) ? !recording.releases.every(hasSecondaryType)
: recording.releases.some(hasSecondaryType);
})) return false;
if (!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));
})) return false;
if (!recordingDate(recording) && dateRequired) 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 (media && params.lookupArtistsByRecording && 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 addAlias(entity, mbid, name, typeId) {
if (!entity || !mbid || !name) return Promise.reject('Invalid argument');
const postData = new URLSearchParams({ 'edit-alias.name': capitalizeName(name) });
if (typeId == 1) postData.set('edit-alias.sort_name', capitalizeName(name));
if (typeId) postData.set('edit-alias.type_id', typeId);
postData.set('edit-alias.edit_note', 'Auto-imported from Discogs by ' + scriptSignature);
if (params.createEntries < 2) postData.set('edit-alias.make_votable', 1);
return globalXHR([mbOrigin, entity, mbid, 'add-alias'].join('/'), { responseType: null }, postData);
}
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]);
});
postData.set(`edit-${entity}.edit_note`, 'Auto-imported from Discogs by ' + scriptSignature);
if (params.createEntries < 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(getGID).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, ')');
}
return gid;
});
});
const extractYear = (entry, words, distance = 30) => entry && entry.profile && words
&& (entry = new RegExp(`\\b(?:${words})\\b.{1,${distance}}\\b((?:19|20)\\d\\d)\\b`, 'i')
.exec(entry.profile)) != null ? parseInt(entry[1]) : 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 && cmpNorm(url).includes(cmpNorm(name))) linkTypeId = 183;
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 console.warn('Undetermined link type for %s. New %s probably won\'t be accepted.', url, entity);
}
const name = capitalizeName(stripDiscogsNameVersion(artist.name.trim()));
const 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|[ck]oro|chor(?:us)?|singers)\b/i,
/^(The|Da|Le|Der|Die|Das|El|La|DJ|MC)\s+/,
/\b(?:Project)\b/i,
];
if (rxs[3].test(name)/* || rxs[1].test(artist.profile)*/) edit.type_id = 5;
else if (rxs[4].test(name)/* || rxs[0].test(artist.profile)*/) edit.type_id = 6;
else if (artist.members && artist.members.length > 0 || rxs[1].test(name)
|| artist.realname && 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;
//else if (!edit.type_id && rxs[2].test(artist.profile)) edit.type_id = 4;
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 = (m = name.split(/\s+/)).pop();
if (m.length > 0) edit.sort_name += ', ' + m.join(' ');
//postData.set(`edit-${entity}.gender_id`, 0); // 1=M. 2=F, 3=🤷, 4=not applicable, 5=other
} else edit.sort_name = name;
if (!edit.type_id) {
const words = stripDiscogsNameVersion(artist.name.trim()).split(/\s+/), rxs = [
/^["„]?[']?\p{L}(?:[\-']?\p{L})*[']?\.?["“]?$/u,
/^[']?\p{L}(?:[\-']?\p{L})+[']?$/u,
];
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 ? 0 : 1].test(word)))
edit.type_id = -1;
else if (rxs[6].test(name)) edit.type_id = -1;
else if (params.createUncertainArtists) edit.type_id = -2;
}
if (edit.type_id != 1 && artist.profile && /^.{0,15}\b(?:unidentified|unknown)\b/i.test(artist.profile))
return mbidUnknown;
if (!edit.type_id) return Promise.reject('Undeterminable sort name');
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, 'Born')) > 0
|| edit.type_id != 1 && (m = extractYear(artist, 'Established')) > 0)
postData.set(`edit-${entity}.period.begin_date.year`, m);
if (!(edit.type_id > 1) && (m = extractYear(artist, 'Died|Deceased')) > 0
|| edit.type_id != 1 && (m = extractYear(artist, 'Dissolved')) > 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;
const nameNorm = name => name && toASCII(stripDiscogsNameVersion(name));
const namedBy = (group, member) => new RegExp('\\b' + nameNorm(member)
.replace(/[^\w\s]/g, '\\$&') + '\\b', 'i').test(nameNorm(group));
if (artist.aliases) artist.aliases.forEach(function(alias) {
workers.push(resolverAdapter(relationResolver(entity, alias), {
'link_type_id': 108,
'backward': sameStringValues(stripDiscogsNameVersion(alias.name), artist.realname) ? 1 : 0,
'period.ended': periodEnded(alias),
}));
});
if (artist.members) artist.members.forEach(function(member) {
const mbidResolver = relationResolver(entity, member);
workers.push(resolverAdapter(mbidResolver, {
'link_type_id': 103,
'backward': 1,
'period.ended': periodEnded(member),
}));
if (namedBy(artist.name, member.name))
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) {
const mbidResolver = relationResolver(entity, group);
workers.push(resolverAdapter(mbidResolver, {
'link_type_id': 103,
'backward': 0,
'period.ended': periodEnded(group),
}));
if (namedBy(group.name, artist.name))
workers.push(resolverAdapter(mbidResolver, { 'link_type_id': 973, 'backward': 1 }),
resolverAdapter(mbidResolver, { 'link_type_id': 895, 'backward': 0 }));
});
return create(artist).then(function(mbid) {
const aliasWorkers = artist.namevariations ? artist.namevariations
.filter(anv => anv.toLowerCase() != name.toLowerCase())
.map(anv => addAlias(entity, mbid, anv, 1)) : [ ];
if (edit.type_id == 1 && artist.realname && artist.realname.toLowerCase() != name.toLowerCase())
aliasWorkers.push(addAlias(entity, mbid, artist.realname, 2));
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) });
if (edit.type_id == -2) GM_openInTab([mbOrigin, 'artist', mbid].join('/'), true);
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 && cmpNorm(url).includes(cmpNorm(stripDiscogsNameVersion(label.name.trim()))))
linkTypeId = 219;
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 console.warn('Undetermined link type for %s. New %s probably won\'t be accepted.', url, entity);
}
addUrl([dcOrigin, discogsEntity(entity), label.id].join('/'));
if (label.urls) label.urls.forEach(addUrl);
if (label.profile) {
disambiguation = normProfile(label.profile.trim().replace(/\r?\n[\S\s]*$/, '').trimRight());
if (disambiguation) disambiguation = translateDiscogsMarkup(disambiguation, false);
const extractDate = words =>
new RegExp(`\\b(?:${words})\\b.{1,30}\\b((?:19|20)\\d\\d)\\b`, 'i').exec(label.profile);
if ((m = extractYear(label, 'Established|Founded')) > 0)
postData.set(`edit-${entity}.period.begin_date.year`, m);
if ((m = extractYear(label, 'Defunct|Ended')) > 0) {
postData.set(`edit-${entity}.period.end_date.year`, m);
postData.set(`edit-${entity}.period.ended`, 1);
}
let 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 && cmpNorm(url).includes(cmpNorm(stripDiscogsNameVersion(series.name.trim()))))
linkTypeId = 745;
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 console.warn('Undetermined link type for %s. New %s probably won\'t be accepted.', url, entity);
}
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 || (edit.type_id = GM_getValue('default_series_type'))) > 0))
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 && cmpNorm(url).includes(cmpNorm(stripDiscogsNameVersion(place.name.trim()))))
linkTypeId = 363;
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 console.warn('Undetermined link type for %s. New %s probably won\'t be accepted.', url, entity);
}
function guessPlaceType(value) {
if (!value) return;
if (/\b(?:Studios?)\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) {
if (!edit.type_id) guessPlaceType(place.profile);
disambiguation = normProfile(place.profile.trim().replace(/\r?\n[\S\s]*$/, '').trimRight());
const extractDate = words =>
new RegExp(`\\b(?:${words})\\b.{1,30}\\b((?:19|20)\\d\\d)\\b`, 'i').exec(place.profile);
if ((m = extractYear(place, 'Established|Founded')) > 0)
postData.set(`edit-${entity}.period.begin_date.year`, m);
if ((m = extractYear(place, 'Defunct|Ended')) > 0) {
postData.set(`edit-${entity}.period.end_date.year`, m);
postData.set(`edit-${entity}.period.ended`, 1);
}
}
if (disambiguation) disambiguation = translateDiscogsMarkup(disambiguation, false);
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(results =>
Promise.all(results[entity.replace(/(?<!s)$/i, 's')].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)
|| entity == 'label' && equal(stripDiscogsNameVersion(lookupIndexes[entity][discogsId].name),
label => label && toASCII(label.replace(...rxBareLabel)).toLowerCase());
}).map(result => mbApiRequest(entity + '/' + result.id, { inc: `aliases+url-rels+${entity}-rels` }).then(function(entry) {
const discogsIds = getDiscogsRels(entry, discogsEntity(entity));
if (discogsIds.includes(parseInt(discogsId))) return discogsIds.length < 2 ? entry.id : true;
return discogsIds.length <= 0;
}).catch(reason => true))), reason => null).then(function(results) {
if (results != null) {
const discogsIds = results.filter(mbIdExtractor);
if (discogsIds.length == 1) return discogsIds[0];
if (results.filter(Boolean) > 0) return Promise.reject('Name collision');
}
return 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 (params.createEntries > 0 && 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(reason => 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 = -1, relationWorkers = [ ];
Object.keys(lookupIndexes).forEach(function(entity, ndx1) {
Object.keys(lookupIndexes[entity]).forEach(function(discogsId, ndx2) {
function addRelation(linkTypeId, attributes, { creditedAs, backward = false, extraData } = { }) {
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]);
}
const mbid = lookupResults[ndx1][ndx2];
if (mbid != null) for (let context of lookupIndexes[entity][discogsId].contexts) switch(entity) {
case 'series':
relationWorkers.push((relationResolvers[context]
|| (relationResolvers[context] = mbApiRequest('series/' + mbid).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.series[linkTypeId][context]) > 0)
return linkTypeId;
console.log('Unsupported series type for relating:', mbSeries.type);
return -1;
}, console.error))).then(function(linkTypeId) {
if (!relateAtLevel(findRelationLevel(linkTypeId)))
return GM_openInTab([mbOrigin, entity, mbid, 'edit'].join('/'), true);
const series = release.series.find(series => series.id == parseInt(discogsId));
console.assert(series, discogsId, release.series);
if (series && series.catno) {
var number = series.catno.trim().replace(/^(?:Vol(?:ume|\.)?|#|N(?:umber|o\.?)|№|N°)\s*(?=\d+$)|\.$/gi, '');
number = /^(?:\d+)$/.test(number) ? parseInt(number) : romanToArabic(number);
}
addRelation(linkTypeId, number > 0 ? [{
id: 'a59c5830-5ec7-38fe-9a21-c7ea54f6650a',
value: number,
}] : null, { backward: true });
}).catch(console.log));
console.info('MBID for series %s:', lookupIndexes[entity][discogsId].name, mbid);
break;
case 'label': case 'place':
if (entity in credits && context in credits[entity]) {
let levels = findRelationLevels(entity, context);
if (levels.some(isTrackLevel) && levels.some(isReleaseLevel))
levels = levels.filter(params.preferTrackRelations ? isTrackLevel : isReleaseLevel);
for (let linkTypeId of levels.map(level => relationsIndex[entity][level][context])) {
console.assert(linkTypeId > 0, entity, context);
if (relateAtLevel(findRelationLevel(linkTypeId))) addRelation(linkTypeId,
[998, 999].includes(linkTypeId) ? [taskAttribute(context)] : null);
}
console.info('MBID for %s %s:', context, lookupIndexes[entity][discogsId].name, mbid);
} else formData.set(context + '.mbid', mbid);
break;
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) {
const hasEA = root => Array.isArray(root = resolveExtraArtists([root],
role => role == context && !levels.some(isReleaseLevel))) && root.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 creditAtTrackLevel = 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) != creditAtTrackLevel) 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': if (![60, 149].includes(linkTypeId)) 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: '63daa0d3-9b63-4434-acff-4977c07808ca' }); break;
case 'Soprano Vocals': attributes.push({ id: 'e88f0be8-a07e-4c0d-bd06-e938eea4d5f6' }); break;
case 'Speech': case 'Narrator': case 'Commentator': case 'Dialog': case 'Interviewer': case 'Interviewee': case 'Proofreader': case 'Read By': attributes.push({ id: 'd3a36e62-a7c4-4eb9-839f-adfebe87ac12' }); 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;
// other vocals
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':
attributes.push({ id: 'c359be96-620a-435c-bd25-2eb0ce81a22e' });
break;
case 'Choir': case 'Chorus': case 'Coro': attributes.push({ id: '43427f08-837b-46b8-bc77-483453af6a7b' }); 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);
if ([44, 51, 60, 148, 149, 156, 305, 759, 760].includes(linkTypeId)
&& extraArtist.modifiers && extraArtist.modifiers.includes('Guest'))
attributes.push({ id: 'b3045913-62ac-433e-9211-ac683cdf6b5c' });
if ([44, 51, 60, 148, 149, 156].includes(linkTypeId)
&& extraArtist.modifiers && extraArtist.modifiers.includes('Soloist'))
attributes.push({ id: '63daa0d3-9b63-4434-acff-4977c07808ca' });
const creditedAs = lookupIndexes[entity][discogsId].anv ?
capitalizeName(lookupIndexes[entity][discogsId].anv) : undefined;
if (creditAtTrackLevel) media.forEach(function(medium, mediumIndex) {
medium.tracks.forEach(function(track, trackIndex) {
if (isCredited(track)) addRelation(linkTypeId, attributes, {
creditedAs: creditedAs,
extraData: { medium: mediumIndex, track: trackIndex },
});
});
}); else addRelation(linkTypeId, attributes, { creditedAs: creditedAs });
}
}).catch(console.log));
console.info('MBID for %s %s:', context, lookupIndexes[entity][discogsId].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:"' + stripDiscogsNameVersion(artist.name) +
'" OR creditname:"' + (artist.anv || stripDiscogsNameVersion(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')
|| descriptors.some(RegExp.prototype.test.bind(/^(?:mix(?:ed|tape)|remix|RMX)\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(track));
})) return recordingsLookup(medium, track, getMBID).then(function(recordings) {
if ((recordings = recordings.filter(recording => !/\b(?:(?:re)?mix(?:ed)?|RMX|rework(?:ed)?|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 origin = 'https://www.allmusic.com';
const idExtractor = (url, entity) => url && entity
&& (entity = new RegExp(`\\bm${entity}\\d{10}\\b`, 'i').exec(url)) != null ? entity[0] : undefined;
return globalXHR(origin + '/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'), origin).href }
catch(e) { console.warn(e) }
}
const textMapper = elem => elem instanceof Element ? elem.textContent.trim() : undefined;
const artistMapper = elem => elem instanceof Element ? {
name: textMapper(elem),
id: idExtractor(elem.href, 'n'),
} : undefined;
const ajaxAdapter = url => globalXHR(url, { headers: { Referer: response.finalUrl } })
.then(({document}) => document.body).catch(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'),
url: response.finalUrl,
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')),
};
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) throw 'Invalid argument';
params = Object.assign({
tracklist: true,
mbidLookup: true, recordingsLookup: true, lookupArtistsByRecording: true, rgLookup: true,
searchSize: GM_getValue('mbid_search_size', 30),
languageIdentifier: true,
openInconsistent: GM_getValue('open_inconsistent', true),
assignUncertain: GM_getValue('assign_uncertain', false),
extendedMetadata: false, releaseRelations: false, recordingRelations: false, workRelations: false,
}, params);
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'; else if (!entry.id) return;
if (!(entity in lookupIndexes)) lookupIndexes[entity] = { };
if (!(entry.id in lookupIndexes[entity])) lookupIndexes[entity][entry.id] = {
name: entry.name,
contexts: [context],
}; else if (!lookupIndexes[entity][entry.id].contexts.includes(context))
lookupIndexes[entity][entry.id].contexts.push(context);
}
function sameArtists(...roots) {
if (roots.length < 2 && !roots.includes(release)) roots.unshift(release);
const sameArtists = (...artists) => artists.length > 0 && (prop =>
artists.every(artist1 => artists.every(artist2 => artist2[prop] == artist1[prop])))
(artists.every(artist => artist.id) ? 'id' : 'name');
return roots.length > 0 && ['artists', 'featArtists'].every(propName =>
roots.every(entry1 => roots.every(entry2 => (entry2[propName] || [ ]).every(artist2 =>
(entry1[propName] || [ ]).some(artist1 => sameArtists(artist1, artist2))))));
}
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));
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 (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((extraArtist, index, array) =>
{ seedArtist(extraArtist, 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 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) {
const timeStamp = Date.now();
if (!(amBindingsCache = GM_getValue('allmusic_to_mb_bindings')))
amBindingsCache = { artist: { }, label: { } };
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]) saveToCache(entity, allMusicId, response); //return Promise.reject('Location changed');
return response;
});
}
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 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';
}
function getAllMusicArtistReleases(releaseType = 1, ajax) {
const url = [amOrigin, amEntity(entity), allMusicId, 'discographyAjax'];
if (ajax) url.push(ajax);
return globalXHR(url.join('/'), {
headers: { Referer: [amOrigin, amEntity(entity), allMusicId].join('/') },
}).then(({document}) => Array.from(document.body.querySelectorAll('table.discographyTable > tbody > tr'), function(album) {
const [year, title] = ['td.year', 'td.meta > span.title > a:first-of-type']
.map(selector => album.querySelector(selector));
if (title != null) return {
id: allMusicIdExtractor(resolveUrl(title), 'album') || allMusicIdExtractor(resolveUrl(title), 'album/release'),
year: year != null ? parseInt(year.textContent) : undefined,
title: title.textContent.trim(),
releaseType: releaseType,
};
}).filter(Boolean), console.error);
}
if (!entity || !allMusicId || !Array.isArray(mbids)) return Promise.reject('Invalid argument');
if (mbids.length <= 0) return Promise.reject('No MusicBrainz entries');
const amReleasesWorker = Promise.all([
getAllMusicArtistReleases(1, 'main'),
getAllMusicArtistReleases(2, 'compilations'),
getAllMusicArtistReleases(3, 'singles'),
getAllMusicArtistReleases(4, 'others'),
]).then(discography => Array.prototype.concat.apply([ ], discography.filter(Boolean)));
const amSongsWorker = (function getAllMusicArtistSongs(page = 1) {
const url = [amOrigin, amEntity(entity), allMusicId, 'songsAjax', 'all', page];
return globalXHR(url.join('/'), {
headers: { Referer: [amOrigin, amEntity(entity), allMusicId].join('/') },
}).then(function({document}) {
const songs = Array.from(document.body.querySelectorAll('div.singleSongResult'), function(song) {
const title = song.querySelector('span.songTitle > a');
if (title != null) return {
id: allMusicIdExtractor(resolveUrl(title), 'song'),
title: title.title || title.textContent.trim(),
};
}).filter(Boolean);
return document.querySelector('div#paginationContainer > button.paginationNext:not(.disabled)') != null ?
getAllMusicArtistSongs(page + 1).then(Array.prototype.concat.bind(songs)) : songs;
}, console.error);
})();
const getReleaseGroups = (mbid, params) => Promise.all(params.map(param =>
mbLookupById('release', param, mbid, ['aliases', 'release-groups', 'url-rels', 'release-group-level-rels'])
.then(releases => releases.filter(release => release['release-group'])
.map(release => Object.assign({ relationType: param }, release['release-group'])), reason => (console.warn(reason), null))))
.then(results => (results = Array.prototype.concat.apply([ ],
results.filter(Boolean))).length > 0 ? results : null);
const lookupMethods = [{
worker: {
artist: mbid => getReleaseGroups(mbid, ['artist', 'track_artist']),
label: mbid => getReleaseGroups(mbid, [entity]),
}[entity],
resolver: releaseGroups => amReleasesWorker.then(function(amAlbums) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'releases'].join('/'), true);
GM_openInTab([amOrigin, amEntity(entity), allMusicId].join('/'), true);
}
const mutualScores = releaseGroups.map(releaseGroups => releaseGroups ? releaseGroups.reduce(function(score, releaseGroup) {
let albums = Array.prototype.concat.apply([ ], getAllMusicRels(releaseGroup, 'album')
.map(allMusicId => amAlbums.filter(albums => albums.id == allMusicId)));
if (albums.length <= 0 && (!releaseGroup['first-release-date']
|| !releaseGroup.title || (albums = amAlbums.filter(function(amAlbum) {
if (!(amAlbum.year > 0) || amAlbum.year != getReleaseYear(releaseGroup['first-release-date']))
return false;
return sameTitleMapper(releaseGroup, amAlbum.title, sameStringValues, releaseTitleNorm);
})).length <= 0)) return score;
if (debugLogging) console.debug('Found matching releases:', releaseGroup, albums);
return score + (releaseGroup.relationType == 'track_artist'
|| albums.every(album => album.releaseType == 2) ? 1/2 : 1);
}, 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);
if (!(hiScore >= 1)) if (params.assignUncertain) openUncertain();
else return Promise.reject('Not found by common releases');
const dataSize = Math.min(amAlbums.length, releaseGroups[hiIndex].length);
if (!(hiScore * 50 >= dataSize)) if (!params.assignUncertain)
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:\n%s\n%s',
hiScore, [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 (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (releases)');
beep.play();
if (params.openInconsistent) openInconsistent(entity, allMusicId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), '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') });
saveToCache(entity, allMusicId, mbids[hiIndex]);
return mbids[hiIndex];
}),
}, {
worker: { artist: getArtistRecordings }[entity],
resolver: mbRecordings => amSongsWorker.then(function(amSongs) {
function openUncertain() {
GM_openInTab([mbOrigin, entity, mbids[hiIndex], 'recordings'].join('/'), true);
GM_openInTab([amOrigin, amEntity(entity), allMusicId].join('/'), true);
}
const mutualScores = mbRecordings.map(recordings =>
[amSongs, recordings].every(Array.isArray) ? recordings.reduce(function(score, recording) {
if (!recording) return score;
let songs = amSongs.filter(amSong =>
sameTitleMapper(recording, amSong.title, sameStringValues, title => title && [
/\s+\((?:(?:feat(?:\b|\.|uring)|ft\.).+|.+\b(?:re)?mix|live|(?:en|ao) (?:vivo|directo?))\)$/i,
].reduce((str, rx) => str.replace(rx, ''), title.trim())));
if (songs.length <= 0) return score;
if (debugLogging) console.debug('Found matching releases:', recording, releases);
return score + 0.5 + (songs.length - 1) * 0.25;
}, 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 recordings');
const hiIndex = mutualScores.indexOf(hiScore);
if (!(hiScore >= 1)) if (params.assignUncertain) openUncertain();
else return Promise.reject('Not found by common recordings');
const dataSize = Math.min(amSongs.length, mbRecordings[hiIndex].length);
if (!(hiScore * 50 >= dataSize)) if (!params.assignUncertain)
return Promise.reject('Matched by common recordings 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], '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));
if (mutualScores.reduce((sum, score) => sum + score, 0) >= hiScore * 1.5)
return Promise.reject('Ambiguity (recordings)');
beep.play();
if (params.openInconsistent) openInconsistent(entity, allMusicId,
mutualScores.map((score, index) => score > 0 && mbids[index]).filter(Boolean), 'recordings');
}
allMusicName(entity, allMusicId).then(name =>
{ notify(`MBID for ${entity} ${name} found by score <b>${hiScore.toFixed(1)}</b> out of ${mbRecordings.length} recording(s)`, 'gold') });
saveToCache(entity, allMusicId, mbids[hiIndex]);
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(mbEntry) {
const allMusicIds = getAllMusicRels(mbEntry, amEntity(entity));
if (allMusicIds.includes(allMusicId)) return allMusicIds.length < 2 ? mbEntry.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') });
saveToCache(entity, allMusicId, lookupMethod[0]);
return lookupMethod[0];
} else lookupMethod = (methodIndex = 0) => methodIndex < lookupMethods.length ?
(['worker', 'resolver'].every(fn => typeof lookupMethods[methodIndex][fn] == 'function') ?
Promise.all(statuses.map((status, mbidIndex) => status
&& lookupMethods[methodIndex].worker(mbids[mbidIndex]).catch(reason => null)))
.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();
});
}
function findMBID(entity, allMusicId) {
let promise = getCachedMBID(entity, allMusicId).catch(reason => findAllMusicRelatives(entity, allMusicId).then(function(entries) {
console.assert(entries.length > 0);
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$)/, '')].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 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', 'blue');
return 1;
}
}
return -1;
}
if (debugLogging) console.debug('AllMusic release metadata for %s:', allMusicId, release);
if (!params.mbidLookup) params.searchSize = 0;
if (params.recordingRelations || params.workRelations) params.tracklist = true;
const literals = { }, lookupIndexes = { };
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 hasType = (...types) => types.some(type => formData.getAll('type').includes(type));
formData.set('name', normTitle(release.title));
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');
if (release.labels) release.labels.forEach(function(label, index) {
const prefix = 'labels.' + index;
if (label.name) {
if (rxNoLabel.test(label.name))
formData.set(prefix + '.mbid', '157afde4-4bf5-4039-8ad2-5a15acc85176');
else {
formData.set(prefix + '.name', capitalizeName(label.name));
if (label.id) 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 artistCredits = release.artistCredits/* || release.mainAlbum && release.mainAlbum.artistCredits*/;
if (params.tracklist) if (media != null) media.forEach(function(medium, mediumIndex) {
if (medium.format || release.format)
formData.set(`mediums.${mediumIndex}.format`, medium.format || release.format);
if (medium.title) formData.set(`mediums.${mediumIndex}.name`, normTitle(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', normTitle(track.title));
frequencyAnalysis(literals, track.title);
}
if (!sameArtists(release, track)) 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 (Object.keys(literals).length > 0) guessTextRepresentation(formData, literals);
const languageDetector = params.languageIdentifier && media
&& languageIdentifier(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') });
let urlRelIndex = -1, relationResolvers = { }, relationsIndex = {
'work': { 'composer': 55 },
'recording': { },
'release': { 'producer': 30, 'mastering': 42 },
'release-group': { },
};
const findRelationLevels = role =>
Object.keys(relationsIndex).filter(level => role in relationsIndex[level]);
addUrlRef([amOrigin, 'album/release', release.id].join('/'), 755);
if (params.rgRelations && release.mainAlbum != null)
addUrlRef([amOrigin, 'album', release.mainAlbum.id].join('/'), 284);
const relateAtLevel = sourceEntity => ({
'work': params.workRelations,
'recording': params.recordingRelations,
'release': params.releaseRelations,
'release-group': params.rgRelations,
}[sourceEntity]);
if (artistCredits != null) {
for (let role in artistCredits.extraArtists) {
const levels = findRelationLevels(role);
if (levels.length <= 0 && !(role in relationResolvers)) {
const resolvers = [ ];
if (params.recordingRelations) resolvers.push(() => instrumentResolver(role).then(instrumentMapper));
for (let resolver of resolvers) if (!relationResolvers[role]) relationResolvers[role] = resolver();
else relationResolvers[role] = relationResolvers[role].catch(resolver);
}
if (levels.length > 0 ? levels.some(relateAtLevel) : role in relationResolvers)
for (let extraArtist of artistCredits[role]) addLookupEntry('artist', extraArtist, role);
}
if ('dj mix' in artistCredits.extraArtists) formData.append('type', 'DJ-mix');
}
purgeArtists();
if (release.releaseTypes) for (let releaseType of release.releaseTypes) switch (releaseType) {
}
if (hasType('DJ-mix') || hasType('Compilation') && formData.get('artist_credit.names.0.mbid') == mbidVA)
formData.set('artist_credit.names.0.mbid', mbidVA);
else seedArtists(release);
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from AllMusic release id ${release.id}`).trimLeft());
const rgLookupWorkers = [ ];
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);
}
return Promise.all([
getSessions(torrentId).catch(reason => null).then(function(sessions) {
function recordingsLookup(medium, track, mbidLookupFn) {
if (!track) throw 'Invalid argument';
if (media == null) return Promise.reject('Missing media');
if (!track.title) return Promise.reject('Missing track name');
const mediumIndex = media.indexOf(medium);
const trackIndex = medium && Array.isArray(medium.tracks) ? medium.tracks.indexOf(track) : -1;
console.assert(mediumIndex >= 0 && trackIndex >= 0);
if (layoutMatch > 2) var trackLength = (function getLengthFromTOC() {
if (!sessions || !(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) && track.duration) {
if ((trackLength = /^(\d+):(\d+)$/.exec(track.duration)) != null)
trackLength = trackLength.slice(1).reverse()
.reduce((s, t, n) => s + parseInt(t) * 60**n, 0) * 1000;
console.assert(trackLength > 0, track.length);
}
if (typeof mbidLookupFn != 'function') mbidLookupFn = undefined;
if (!(trackLength > 0) && !mbidLookupFn) return Promise.reject('Missing track length');
const maxLengthDifference = 5000;
const artists = trackMainArtists(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 ')})`);
fields.push('video:false');
return fields.join(' AND ');
})(),
limit: 100,
}).then(function(recordings) {
function recordingValidator(recording, lengthRequired = false, dateRequired = false) {
if (recording.score < 90 || recording.video) return false;
if (!sameStringValues(recording.title, track.title) && ['(?:re-?)?mix(?:ed)?|RMX'].some(function(rx) {
rx = new RegExp('\\s+\\(.*\\b(?:' + rx + ')\\b.*\\)$', 'i');
let recordingFlag = rx.test(recording.title), trackFlag = rx.test(track.title);
return recordingFlag != trackFlag;
})) return false;
if (recording.releases && ['Live', 'Interview', 'Demo'].some(function(secondaryType) {
const hasSecondaryType = release => 'release-group' in release
&& 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType);
return hasType(secondaryType) ? !recording.releases.every(hasSecondaryType)
: recording.releases.some(hasSecondaryType);
})) return false;
if (!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));
})) return false;
if (!recordingDate(recording) && dateRequired) 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 artistLookupWorkers = { };
if (media != null && params.lookupArtistsByRecording && params.searchSize > 0)
for (let medium of media) for (let track of medium.tracks) (function addArtistLookups(artists) {
if (artists != null) for (let artist of artists) if (artist.id) {
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;
}));
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 => parseInt(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) {
const mbid = lookupResults[ndx1][ndx2];
if (mbid != null) for (let context of lookupIndexes[entity][allMusicId].contexts) switch(entity) {
case 'artist':
if (context in artistCredits) {
const levels = findRelationLevels(context);
const resolver = levels.length > 0 ? Promise.resolve(levels.map(level =>
({ linkTypeId: relationsIndex[level][context] }))) : relationResolvers[context];
relationWorkers.push(resolver.then(function(relations) {
console.assert(relations.length > 0);
for (let { linkTypeId, attributes, creditType = context } of relations) {
if (!(linkTypeId > 0)) continue; // not to be related
const sourceLevel = findRelationLevel(linkTypeId), attrIds = [ ];
if (relateAtLevel(sourceLevel)) switch (creditType) {
} else continue;
if (attrIds.length > 0) {
if (!attributes) attributes = [ ];
Array.prototype.push.apply(attributes, attrIds.map(id => ({ id: id })));
}
const prefix = 'rel.' + ++relIndex;
formData.set(`${prefix}.entity`, entity);
formData.set(`${prefix}.link_type_id`, linkTypeId);
formData.set(`${prefix}.target`, mbid);
formData.set(`${prefix}.name`, stripDiscogsNameVersion(lookupIndexes[entity][allMusicId].name));
if ((attributes = attributes.filter(attribute => attribute.id)).length > 0)
formData.set(`${prefix}.attributes`, JSON.stringify(attributes));
}
}).catch(console.log));
console.info('MBID for %s %s:', context, lookupIndexes[entity][allMusicId].name, mbid);
break;
}
default:
formData.set(context + '.mbid', mbid);
}
});
});
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 (media == null) return false;
const isOriginalRecording = hasType('DJ-mix', 'Remix', 'Live');
if (params.recordingsLookup <= 0) return false; else if (!(params.recordingsLookup > 1))
if (isOriginalRecording || formData.has('release_group')) return false;
return Promise.all(media.map(function(medium, mediumIndex) {
if (!medium || !Array.isArray(medium.tracks)) return false;
return Promise.all(medium.tracks.map((track, trackIndex) =>
recordingsLookup(medium, track, getMBID).then(function(recordings) {
if ((recordings = recordings.filter(recording =>
!/\b(?:(?:re)?mix|rework|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));
});
}),
languageDetector,
]).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') == mbidVA && !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';
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_torrent_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';
return editNote + '\nSubmitted by ' + scriptSignature;
}
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);
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) }, params);
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.overwrite || !mbRelease.status) {
let statusId = formData.get('status');
if (statusId = findEditId('status', statusId) || statusId)
batchEdits[32].status_id = statusId;
}
if (params.overwrite || !mbRelease.packaging) {
let packagingId = formData.get('packaging');
if (packagingId = findEditId('packaging', packagingId) || packagingId)
batchEdits[32].packaging_id = packagingId;
}
if (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.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.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;
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 (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 (!mbRelease.disambiguation && formData.get('comment'))
batchEdits[32].comment = formData.get('comment');
if (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 (!mbRelease.annotation && formData.get('annotation')) edits.push({
edit_type: 35,
entity: mbRelease.id,
text: formData.get('annotation'),
});
const createEntries = GM_getValue('mb_create_entries', 2);
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 && ![name, capitalizeName(name)].includes(creditedAs))
edit[`entity${backward ? 1 : 0}_credit`] = creditedAs;
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, 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
|| relation[targetEntity].id.toLowerCase() != target.toLowerCase()) return false;
const hasType = (...linkTypeIds) => linkTypeIds.some(linkTypeId => relation.type == mbRelationsIndex[relationLevel][linkTypeId]);
return (hasType(linkTypeId) || (() => { switch (linkTypeId) { // relation ids that should not coexist
// 54: 'writer', 55: 'composer', 56: 'lyricist', 57: 'librettist',
// 167: 'writer', 168: 'composer', 165: 'lyricist', 169: 'librettist',
case 54: case 167: return hasType(55, 56, 57, 165, 168, 169);
case 55: case 168: return hasType(54, 167);
case 56: case 165: return hasType(54, 57, 167, 169);
case 57: case 169: return hasType(54, 56, 165, 167);
default: return false;
} })()) && (!attributes || attributes.every(attribute => Object.values(relation['attribute-ids'])
.some(id => id.toLowerCase() == attribute.id.toLowerCase())));
});
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);
workResolver = function(id, typeId, name, params) {
if (workWorkers.has(id)) return workWorkers.get(id);
let rx = [
'live|(?:en|ao) (?:vivo|directo?)|instrumental',
'(?:re)?mix|RMX|edit|version|soundtrack|score',
];
rx = new RegExp('(?:\\s+(?:' + ['()', '[]'].map(br => `\\${br[0]}(?:(?:` +
rx[0] + `)\\b[^\\${br[0]}\\${br[1]}]*|[^\\${br[0]}\\${br[1]}]*\\b(?:` +
rx[1] + `))\\${br[1]}`).join('|') + '))+$', 'gi');
if (name) name = name.replace(rx, ''); else return Promise.reject('Work name is missing');
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+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 (!(createEntries > 0)) return Promise.reject('Creating new entries is forbidden');
params = Object.assign({ comment: '' }, params, { edit_type: 41, name: name });
if (typeId > 0) params.type_id = typeId;
if (createEntries < 2) params.make_votable = true;
return mbCreateEdit([params], 'Auto-created by ' + scriptSignature, params.makeVotable).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);
workResolver = workResolver.then(workId => !reusedWorks.has(workId)
|| !hasRelation(reusedWorks.get(workId)) ? workId : Promise.reject('Already related'));
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 null;
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');
// attributes.push('3d984f6e-bbe2-4620-9425-5f32e945b60d'); // karaoke
// attributes.push('37da3398-5d1b-4acb-be25-df95e33e423c'); // medley performance
// 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,
};
}, reason => null));
edits.push(workResolver.then(workId => createEdit({ entityType: relationLevel, gid: workId }),
reason => null));
break;
}
}
});
});
break;
case 'release-group': case 'release':
if (relationLevel == 'release-group' && hasRelation(mbRelease['release-group'])
|| hasRelation(mbRelease)
|| mbRelease?.media.some(medium => medium?.tracks.some(relationOnTrack))) break;
edits.push(createEdit({
'release': { entityType: 'release', gid: mbRelease.id },
'release-group': { entityType: 'release_group', gid: mbRelease?.['release-group'].id },
}[relationLevel]));
break;
default:
console.warn('Assertion failed, unexpected source entity type:', relationLevel, linkTypeId, targetEntity);
}
}
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 added with response code ' + responseCode, 'aquamarine');
console.log('Tags for release %s added with response code', 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);
const editNote = ['Release updated'];
if (sourceRef) editNote.push('from ' + sourceRef);
return edits.length > 0 ? mbCreateEdit(edits, editNote.concat('by ' + scriptSignature).join(' '), params.makeVotable).then(function(edits) {
console.log('Release', mbRelease.id, (edits.every(edit => edit.response == 1) ?
'edits successfull' : 'edits successfull (some rejected)') + ':', edits);
notify(edits.every(edit => edit.response == 1) ? 'Release edits successfull'
: 'Release edits edits successfull (some rejected)', 'mediumturquoise');
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 params = [
['Import tracklist', true],
['Align with TOCs', false, 'Try to find track parsing method by aligning with TOCs (logs in sequential order required)'],
['Compose annotation', GM_getValue('compose_annotation', true)],
['MBID lookup (required to resolve artists & labels, slower)', !evt.shiftKey],
['Release group lookup', !evt.altKey],
['Forced recordings lookup', evt.ctrlKey],
['Make edits votable', !(mbSeedNew >= 2)],
];
input = await promptEx('Seed new MusicBrainz release', prompt && prompt + ':', required, input, params);
if (input == null) return; else if (!input && !required) return callback(target);
let param, id;
if (id = discogsIdExtractor(input, 'release')) param = 'discogsId';
else if (id = allMusicIdExtractor(input, 'album/release')) param = 'allMusicId';
return param ? callback(target, {
[param]: id,
tracklist: params[0][1], alignByTOCs: params[1][1],
composeAnnotation: params[2][1],
mbidLookup: params[3][1],
rgLookup: params[4][1],
recordingsLookup: params[5][1] ? 2 : 1,
makeVotable: params[6][1],
}) : (function() {
if (id = mbIdExtractor(input, 'release-group')) return Promise.resolve(id);
if (id = mbIdExtractor(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 = 'Edition lookup by CD TOC browser script (https://greasyfork.org/scripts/459083)';
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', 1));
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 getArtistRecordings = mbid => Promise.all([
mbLookupById('recording', 'artist', mbid),
mbApiRequest('artist/' + mbid, { inc: 'aliases+recording-rels' }).then(function({relations}) {
if (!relations) throw `Assertion failed: no relations for artist ${mbid}`;
return relations.filter(relation => ['recording'].includes(relation['target-type']))
.map(relation => Object.assign({ relationType: relation.type }, relation[relation['target-type']]))
.filter((recording1, index, recordings) => recordings.findIndex(recording2 => recording2.id == recording1.id) == index);
}).catch(function(reason) {
console.warn(reason);
return null;
}),
]).then(recordings => (recordings = Array.prototype.concat.apply([ ],
recordings.filter(Boolean))).length > 0 ? recordings : null);
const normTitle = title => title && [ ].reduce((str, subst) => str.replace(...subst), title);
const capitalizeName = name => name && [
[/\s+/g, ' '],
[/\s+(And|Of|In|On|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) => [148, 44].map(linkTypeId => ({
linkTypeId: linkTypeId,
attributes: attributes && attributes.length > 0 ? creditedAs ? attributes.map(attribute =>
Object.assign(attribute, { creditedAs: creditedAs.replace(...untitleCase) })) : attributes : null,
creditType: creditType || undefined,
}));
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,
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\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 options = [
['Compose annotation', GM_getValue('compose_annotation', true)],
['Create release level relationships', !evt.shiftKey],
['Create track level relationships', evt.altKey],
['Prefer track level over release level relationships if ambiguous', 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.'],
['Overwrite existing values (use with caution)', evt.ctrlKey],
['Make edits votable (recommended)', !(mbUpdateRelease >= 2)],
];
input = await promptEx('Update MusicBrainz release', prompt + ':', true, input, options);
if (input == null) return; else options = options.map(option => option[1]);
let param, xtrnDbId;
if (xtrnDbId = discogsIdExtractor(input, 'release')) param = 'discogsId';
else if (xtrnDbId = allMusicIdExtractor(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: options[0],
releaseRelations: options[1], rgRelations: options[1],
recordingRelations: options[2], workRelations: options[2],
preferTrackRelations: options[3],
languageIdentifier: params.languageIdentifier,
tracklist: false, recordingsLookup: 0, rgLookup: false, lookupArtistsByRecording: false,
}, cdLengths), dbName + ' release id ' + xtrnDbId, {
overwrite: options[4],
makeVotable: options[5],
}).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)\n--------------------\nKeyboard modifiers:\nAlt - relate artist credits also on recording and work level\nCtrl - force overwrite (use only if existing release data is somehow wrong)\nShift - don\'t lookup any MBIDs (fast but some information will be missing, no relations will be created)\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 != '157afde4-4bf5-4039-8ad2-5a15acc85176' ?
[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().addValues(tocEntries, 8, 100).toDigest());
}).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);
}
}