// ==UserScript==
// @name [GMT] Edition lookup by CD TOC
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.15.19
// @description Lookup edition by CD TOC on MusicBrainz, GnuDb and in CUETools DB
// @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
// @connect musicbrainz.org
// @connect coverartarchive.org
// @connect archive.org
// @connect api.discogs.com
// @connect www.discogs.com
// @connect db.cuetools.net
// @connect db.cue.tools
// @connect gnudb.org
// @connect accuraterip.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
// @downloadURL none
// ==/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 cmpNorm = expr => expr && expr.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.96) => strVal1 && strVal2
&& (sameStringValues(strVal1, strVal2)
|| jaroWrinkerSimilarity(strVal1.toLowerCase(), strVal2.toLowerCase()) >= threshold);
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, '
')
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;
}
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;
}
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;
const debugLogging = GM_getValue('debug_logging', 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 reqHeaders = { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' };
if (dcAuth) reqHeaders.Authorization = 'Discogs ' + dcAuth;
let requestsMax = reqHeaders.Authorization ? 60 : 25, retryCounter = 0;
const request = new Promise((resolve, reject) => (function request() {
const now = Date.now();
const postpone = () => { setTimeout(request, dcApiRateControl.timeFrameExpiry - now) };
if (!dcApiRateControl.timeFrameExpiry || now > dcApiRateControl.timeFrameExpiry) {
dcApiRateControl.timeFrameExpiry = now + 60 * 1000 + 500;
if (dcApiRateControl.requestDebt > 0) {
dcApiRateControl.requestCounter = Math.min(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: reqHeaders,
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();
} 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;
if ('accurateRipOffsets' in sessionStorage) try {
return arOffsets = Promise.resolve(JSON.parse(sessionStorage.getItem('accurateRipOffsets')));
} catch(e) {
console.warn(e);
sessionStorage.removeItem('accurateRipOffsets');
}
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(),
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.submits,
agreeRate: offset.agreeRate,
} })));
//sessionStorage.setItem('accurateRipOffsets', JSON.stringify(offsets));
return offsets;
});
}
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 ', '(?:EAC|XLD) Auslese-Logdatei vom ',
'File di log (?:EAC|XLD) per l\'estrazione del ', 'Archivo Log de extracciones desde ',
'(?:EAC|XLD) extraheringsloggfil från ', '(?:EAC|XLD) uitlezen log bestand van ',
'(?:EAC|XLD) 抓取日志文件从',
'Отчёт (?:EAC|XLD) об извлечении, выполненном ', 'Отчет на (?:EAC|XLD) за извличане, извършено на ',
'Protokol extrakce (?:EAC|XLD) z ', '(?:EAC|XLD) log súbor extrakcie z ',
'Sprawozdanie ze zgrywania programem (?:EAC|XLD) z ', '(?:EAC|XLD)-ov fajl dnevnika ekstrakcije iz ',
'Log created by: whipper .+\r?\n+Log creation date: ', 'morituri extraction logfile from ',
].join('|') + ')';
const rxTrackExtractor = /^(?:(?:Track|Трек|Òðåê|音轨|Traccia|Spår|Pista|Трак|Utwór|Stopa)\s+\d+[^\S\r\n]*$(?:\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(''),
];
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)\\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 ',
].join('|') + ')\\d+)';
const verifyOffsets = sessions => sessions.length > 0 ? Promise.all(sessions.map(function(session) {
const usedDrive = /^(?:Used drive|Benutztes Laufwerk|Unità predefinita|Usar unidad|Използвано устройство|使用驱动器|Použitá mechanika|Använd enhet|gebruikt loopwerk|Użyty napęd|Дисковод|Korišćen drajv)\s*:\s*(.+?)(?:\s{3}(.+?)(?:\s{2}(.+?))?)?$/m.exec(session);
const 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)\s*:\s*(\d+)\b/m.exec(session);
return usedDrive != null && readOffset != null ? getAccuripOffsets().then(function(arOffsets) {
const keys = Object.keys(arOffsets).filter(key => sameStringValues(key, usedDrive[1]));
const signedOffset = offset => offset > 0 ? '+' + offset : offset;
if (keys.length <= 0) return; else if (keys.length > 1) {
console.info('Multiple drives found for', usedDrive[1], keys.map(key => arOffsets[key]),
'(ambiguity - offset not verified)');
return;
} else if (arOffsets[keys[0]].submits < 5 && arOffsets[keys[0]].agreeRate < 100) {
console.info('Weak AR value for', usedDrive[1], '- offset not verified (',
signedOffset(parseInt(readOffset[1])), '/', arOffsets[keys[0]], ')');
return;
}
console.info('AR offset found for %s:', usedDrive[1].replace(/\s+/g, ' '),
signedOffset(arOffsets[keys[0]].offset));
return parseInt(readOffset[1]) == arOffsets[keys[0]].offset;
}, reason => { console.warn(reason) }) : Promise.resolve(undefined);
})).then(results => !results.some(result => result == false) ? sessions : Promise.reject('Incorrect read offset')) : null;
if (!detectVolumes) {
const rxStackedLog = new RegExp('^[\\S\\s]*(?:\\r?\\n)+(?=' + rxRipperSignatures + ')');
return verifyOffsets(logFiles.map(logFile => rxStackedLog.test(logFile) ? logFile.replace(rxStackedLog, '') : logFile)
.filter(RegExp.prototype.test.bind(new RegExp('^(?:' + rxRipperSignatures + '|' + sessionHeader + ')'))));
}
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, rxTitleExtractor = new RegExp('^' + sessionHeader + '(.+)$(?:\\r?\\n)+^(.+\\/.+)$', 'm');
for (const logFile of logFiles) for (const session of logFile) {
let [uniqueKey, title] = [getTocEntries(session), rxTitleExtractor.exec(session)];
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 != null) title = title[2];
else if ((title = /^ +Release: *$\r?\n^ +Artist: *(.+)$\r?\n^ +Title: *(.+)$/m.exec(session)) != null)
title = title[1] + '/' + title[2];
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 (!quotaExceeded && sessions != null) try {
if (!sessionsSessionCache) sessionsSessionCache = { };
sessionsSessionCache[torrentId] = sessions;
sessionStorage.setItem('ripSessionsCache', JSON.stringify(sessionsSessionCache));
} catch(e) {
quotaExceeded = true;
console.warn(e);
}
return sessions || Promise.reject('No valid logfiles attached');
})));
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 [rxNoLabel, rxBareLabel, rxNoCatno] = [
/^(?:Not On Label|No label|\[no label\]|None|\[none\]|iMD|Independ[ae]nt|Self[- ]?Released)\b/i,
/(?:\s+\b(?:Record(?:ing)?s|Productions?|Corporation|Int(?:ernationa|\')l)\b|,?\s+(?:Ltd|Inc|Co(?:rp)?|LLC|Intl)\.?)+$/ig,
/^(?:None|\[none\])$/i,
];
const bareId = str => str ? str.trim().toLowerCase().replace(rxBareLabel, '').replace(rxNoLabel, '').replace(/\W/g, '') : '';
const uniqueValues = ((el1, ndx, arr) => el1 && arr.findIndex(el2 => bareId(el2) == bareId(el1)) == ndx);
function openTabHandler(evt) {
if (!evt.ctrlKey) return true;
if (evt.shiftKey && evt.currentTarget.dataset.groupUrl)
return (GM_openInTab(evt.currentTarget.dataset.groupUrl, false), false);
if (evt.currentTarget.dataset.url)
return (GM_openInTab(evt.currentTarget.dataset.url, false), false);
return true;
}
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 = { };
if (target.dataset.releaseYear) payload.remaster_year = target.dataset.releaseYear; else return false;
if (target.dataset.editionInfo) try {
const editionInfo = JSON.parse(target.dataset.editionInfo);
payload.remaster_record_label = editionInfo.map(label => label.label).filter(uniqueValues)
.map(label => rxNoLabel.test(label) ? 'self-released' : label).filter(Boolean).join(' / ');
payload.remaster_catalogue_number = editionInfo.map(label => label.catNo).filter(uniqueValues)
.map(catNo => !rxNoCatno.test(catNo) && catNo).filter(Boolean).join(' / ');
} catch (e) { console.warn(e) }
if (!payload.remaster_catalogue_number && target.dataset.barcodes) try {
payload.remaster_catalogue_number = JSON.parse(target.dataset.barcodes)
.filter((barcode, ndx, arr) => barcode && arr.indexOf(barcode) == ndx).join(' / ');
} catch (e) { console.warn(e) }
if (target.dataset.editionTitle) payload.remaster_title = target.dataset.editionTitle;
const entries = [ ];
if ('remaster_year' in payload) entries.push('Edition year: ' + payload.remaster_year);
if ('remaster_title' in payload) entries.push('Edition title: ' + payload.remaster_title);
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 (entries.length <= 0 || Boolean(target.dataset.confirm) && !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 = true;
target.style.color = 'orange';
let selector = target.parentNode.dataset.edition;
if (!selector) return (alert('Assertion failed: edition group not found'), false);
selector = 'table#torrent_details > tbody > tr.torrent_row.edition_' + selector;
Promise.all(Array.from(document.body.querySelectorAll(selector), function(tr) {
const torrentId = getTorrentId(tr);
if (!(torrentId > 0)) return null;
const postData = new URLSearchParams(payload);
if (parseInt(target.parentNode.dataset.torrentId) == torrentId && 'description' in target.dataset
&& target.dataset.url) postData.set('release_desc', (target.dataset.description + '\n\n').trimLeft() +
'[url]' + target.dataset.url + '[/url]');
return queryAjaxAPI('torrentedit', { id: torrentId }, postData);
return `torrentId: ${torrentId}, postData: ${postData.toString()}`;
})).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 = 'pointer';
tr.dataset.confirm = true;
tr.onclick = updateEdition;
let tooltip = 'Apply edition info from this release\n(Ctrl + click opens release page';
if (tr.dataset.groupUrl) tooltip += ' / Ctrl + Shift + click opens release group page';
setTooltip(tr, (tooltip += ')'));
tr.onmouseenter = tr.onmouseleave = evt => { evt.currentTarget.style.backgroundColor =
evt.type == 'mouseenter' ? '#FFA50040' : evt.currentTarget.dataset.backgroundColor || null };
}
function openOnClick(tr) {
tr.onclick = openTabHandler;
const updateCursor = evt => { tr.style.cursor = evt.ctrlKey ? 'pointer' : 'auto' };
tr.onmouseenter = function(evt) {
updateCursor(evt);
document.addEventListener('keyup', updateCursor);
document.addEventListener('keydown', updateCursor);
};
tr.onmouseleave = function(evt) {
document.removeEventListener('keyup', updateCursor);
document.removeEventListener('keydown', updateCursor);
};
let tooltip = 'Ctrl + click opens release page';
if (tr.dataset.groupUrl) tooltip += '\nCtrl + Shift + click opens release group page';
setTooltip(tr, tooltip);
}
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 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(`
`);
const svgCheckmark = (color = '#0c0', height = '0.9em') => minifyHTML(`
`);
const svgQuestionMark = (color = '#fc0', height = '0.9em') => minifyHTML(`
`);
const svgAniSpinner = (color = 'orange', phases = 12, height = '0.9em') => minifyHTML(`
`);
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));
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, span.dataset.torrentId] = ['brackets', torrentId];
span.style = 'display: inline-flex; flex-flow: row; align-items: baseline; column-gap: 5px; justify-content: space-around; color: initial;';
if (edition != null) span.dataset.edition = edition;
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 addClickableIcon(html, clickHandler, dropHandler, className, style, tooltip, tooltipster = false) {
if (!html || typeof clickHandler != 'function') throw 'Invalid argument';
const span = document.createElement('span');
span.innerHTML = html;
if (className) span.className = className;
span.style = 'cursor: pointer; transition: transform 100ms;' + (style ? ' ' + style : '');
span.onclick = clickHandler;
if (typeof dropHandler == 'function') {
span.ondragover = evt => Boolean(evt.currentTarget.disabled) || !evt.dataTransfer.types.includes('text/plain');
span.ondrop = function(evt) {
evt.currentTarget.style.transform = null;
if (evt.currentTarget.disabled || !evt.dataTransfer || !(evt.dataTransfer.items.length > 0)) return true;
dropHandler(evt);
return false;
}
span.ondragenter = function(evt) {
if (evt.currentTarget.disabled) return true;
for (let tgt = evt.relatedTarget; tgt != null; tgt = tgt.parentNode)
if (tgt == evt.currentTarget) return false;
evt.currentTarget.style.transform = 'scale(3)';
return false;
};
span[`ondrag${'ondragexit' in span ? 'exit' : 'leave'}`] = function(evt) {
if (evt.currentTarget.disabled) return true;
for (let tgt = evt.relatedTarget; tgt != null; tgt = tgt.parentNode)
if (tgt == evt.currentTarget) return false;
evt.currentTarget.style.transform = null;
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}`);
}
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, '_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.textDecoration = 'underline yellowgreen dotted 2pt';
//elem.style.textShadow = '#9acd32C0 0 0 6pt';
//elem.style.backgroundColor = '#9acd3230';
}
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 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', labelName];
if (labelURL) [elem.href, elem.target, elem.style] = [labelURL, '_blank', noLinkDecoration];
if (Array.isArray(recordLabels) && recordLabels.some(recordLabel =>
sameLabels(recordLabel, labelName))) editionInfoMatchingStyle(elem);
components.push(elem);
}
if (catNo) {
const span = document.createElement('span');
[span.className, span.textContent, span.style] = ['catno', catNo, 'white-space: nowrap;'];
if (Array.isArray(catalogueNumbers) && (catalogueNumbers.some(catalogueNumber =>
sameStringValues(catalogueNumber, catNo)) || sameStringValues(catNo, catalogueNumbers.join('/'))))
editionInfoMatchingStyle(span);
components.push(span);
}
return components;
}
function fillListRows(container, listElements, maxRowsToShow) {
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 divs = createElements('div', 'div');
[divs[0].className, divs[0].style] = ['show-all', '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 findDiscogsRelatives(entity, discogsId, mbEntity = entity) {
if (!entity || !((discogsId = parseInt(discogsId)) > 0) || !mbEntity) throw 'Invalid argument';
const targetType = mbEntity.replace(/-/g, '_');
return mbApiRequest('url', {
query: `url_descendent:*discogs.com/${entity}/${discogsId}`,
limit: 100,
}).then(results => results.count > 0 && (results = results.urls.filter(url =>
discogsIdExtractor(url.resource, entity) == discogsId)).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'));
}
const torrentId = getTorrentId(tr);
if (!(torrentId > 0)) continue; // assertion failed
let edition = /\b(?:edition_(\d+))\b/.exec(tr.className);
if (edition != null) edition = parseInt(edition[1]);
const editionRow = (function(tr) {
while (tr != null) { if (tr.classList.contains('edition')) return tr; tr = tr.previousElementSibling }
return null;
})(tr);
let editionInfo = editionRow && editionRow.querySelector('td.edition_info > strong');
editionInfo = editionInfo != null ? editionInfo.lastChild.textContent.trim() : '';
if (incompleteEdition.test(editionInfo)) editionRow.cells[0].style.backgroundColor = '#f001';
if ((tr = tr.nextElementSibling) == null || !tr.classList.contains('torrentdetails')) continue;
const linkBox = tr.querySelector('div.linkbox');
if (linkBox == null) continue;
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 stripNameSuffix = name => name && name.replace(/\s+\(\d+\)$/, '');
const noLinkDecoration = 'background: none !important; padding: 0 !important;';
const linkHTML = (url, caption, cls) => `${caption}`;
const svgBulletHTML = color => ``;
const createElements = (...tagNames) => tagNames.map(Document.prototype.createElement.bind(document));
const editionInfoSplitter = infoStr =>
infoStr ? (infoStr = decodeHTML(infoStr)).split(/[\/\|\•\;\,]+/)
.map(expr => expr.trim()).filter((s, n, a) => s && a.indexOf(s) == n) : [ ];
const sameLabels = (...labels) => labels.length > 0 && labels.every((label, ndx, arr) =>
cmpNorm(label.replace(rxBareLabel, '')) == cmpNorm(arr[0].replace(rxBareLabel, ''))
|| label.toLowerCase().startsWith(arr[0].toLowerCase()) && /^\W/.test(label.slice(arr[0].length))
|| arr[0].toLowerCase().startsWith(label.toLowerCase()) && /^\W/.test(arr[0].slice(label.length)));
const sameBarcodes = (...barcodes) => barcodes.length > 0
&& barcodes.every((barcode, ndx, arr) => parseInt(cmpNorm(barcode)) == parseInt(cmpNorm(arr[0])));
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(target.parentNode.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 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: 'artist-credits labels release-groups url-rels' };
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 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 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;
}
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) : '';
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', '89ad4ac3-39f7-470e-963a-56509c546377');
else if (torrent.group.musicInfo)
for (let role of ['dj', 'artists']) if (artistIndex < 0)
torrent.group.musicInfo[role].forEach(addArtist);
formData.set('status', torrent.group.releaseType == 14 ? '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{9,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) {
sessions.forEach(function(session, discIndex) {
switch (getLayoutType(getTocEntries(session))) {
case 0: break;
case 1: formData.set(`mediums.${discIndex}.format`, 'Enhanced CD'); break;
case 2: formData.set(`mediums.${discIndex}.format`, 'Copy Control CD'); break;
default: console.warn(`Disc ${discIndex + 1} unknown TOC type`);
}
});
if (GM_getValue('mb_seed_with_fingerprints', false) && Array.isArray(sessions) && sessions.length > 0)
editNote += '\n\nMedia fingerprint' + (sessions.length > 1 ? 's' : '') + ' :\n' +
sessions.map(getMediaFingerprint).join('\n') + '\n';
formData.set('edit_note', editNote);
return formData;
});
}
function seedFromDiscogs(formData, params, cdLengths) {
if (![formData, params].every(param => param && typeof param == 'object')
|| !(params.discogsId > 0)) throw 'Invalid argument';
return dcApiRequest('releases/' + params.discogsId).then(function(release) {
function seedArtists(artists, extraArtists, prefix) {
function seedArtist(artist, index, array, offset = 0) {
const creditPrefix = `${prefix || ''}artist_credit.names.${offset + index}`;
const name = stripNameSuffix(artist.name);
formData.set(`${creditPrefix}.artist.name`, name);
if (artist.anv) formData.set(`${creditPrefix}.name`, artist.anv);
else formData.delete(`${creditPrefix}.name`);
if (index < array.length - 1) formData.set(`${creditPrefix}.join_phrase`,
fmtJoinPhrase(offset == 0 && artist.join || (index < array.length - 2 ? ', ' : ' & ')));
else formData.delete(`${creditPrefix}.join_phrase`);
if (!(artist.id in lookupIndexes.artist)) lookupIndexes.artist[artist.id] = {
name: artist.name,
searchName: name,
prefixes: [creditPrefix],
}; else lookupIndexes.artist[artist.id].prefixes.push(creditPrefix);
}
if (!Array.isArray(artists) || artists.length <= 0) return;
artists.forEach((artist, index, array) => seedArtist(artist, index, array));
if (!Array.isArray(extraArtists) || extraArtists.length <= 0) return ;
const featArtists = extraArtists.filter(extraArtist =>
!artists.some(artist => artist.id == extraArtist.id) && extraArtist.role == 'Featuring');
featArtists.forEach(function(artist, index, array) {
if (index == 0) formData.set(`${prefix || ''}artist_credit.names.${artists.length - 1}.join_phrase`, ' feat. ');
seedArtist(artist, index, array, artists.length);
});
}
function addUrlRef(url, linkType) {
formData.set(`urls.${++urlRelIndex}.url`, url);
if (linkType != undefined) formData.set(`urls.${urlRelIndex}.link_type`, linkType);
}
function mbLookupById(entity, param, mbid) {
if (!entity || !param || !mbid) throw 'Invalid argument';
[entity, param] = [entity.toLowerCase(), param.toLowerCase()];
const loadPage = (offset = 0) => mbApiRequest(entity, { [param]: mbid, inc: 'url-rels aliases', offset: offset, limit: 5000 }).then(function(response) {
let results = response[entity + 's'];
if (Array.isArray(results)) offset = response[entity + '-offset'] + results.length; else return [ ];
results = results.filter(result => !result.video);
return offset < response[entity + '-count'] ? loadPage(offset)
.then(Array.prototype.concat.bind(results)) : results;
});
return loadPage();
}
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());
function notifyFoundMBID(html, color = 'orange', length = 6) {
if (!html) return;
let div = document.body.querySelector('div.mbid-lookup-notify'), animation;
if (div == null) {
div = document.createElement('div');
div.className = 'mbid-lookup-notify';
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: 999; 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 = 'MBID for ' + 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 parseTracks(trackParser, collapseSubtracks = false) {
if (!(trackParser instanceof RegExp)) throw 'Invalid argument';
const media = [ ];
let lastMediumId, heading;
(function addTracks(root, titles) {
function addTrack(track) {
const parsedTrack = trackParser.exec(track.position.trim());
let [mediumFormat, mediumId, trackPosition] = parsedTrack != null ?
parsedTrack.slice(1) : [undefined, undefined, track.position.trim()];
if (!mediumFormat && /^[A-Z]\d*$/.test(trackPosition)) mediumFormat = 'LP';
mediumId = (mediumFormat || '') + (mediumId || '');
if (lastMediumId == undefined || !titles && mediumId !== lastMediumId) {
for (let subst of [[/^(?:B(?:R?D|R))$/, 'Blu-ray'], [/^(?:LP)$/, 'Vinyl']])
if (subst[0].test(mediumFormat)) mediumFormat = subst[1];
media.push({ format: mediumFormat || defaultFormat, name: undefined, tracks: [ ] });
lastMediumId = mediumId;
}
let name = track.title;
if (track.type_ == 'index' && 'sub_tracks' in track && track.sub_tracks.length > 0)
name += ` (${track.sub_tracks.map(track => track.title).join(' / ')})`;
media[media.length - 1].tracks.push({
number: trackPosition, heading: heading, titles: titles, name: name,
length: track.duration, artists: track.artists, extraartists: track.extraartists,
});
}
if (Array.isArray(root)) for (let track of root) switch (track.type_) {
case 'track': addTrack(track); break;
case 'heading':
heading = track.title != '-' && track.title.replace(/:+$/, '') || undefined;
break;
case 'index':
if (collapseSubtracks) addTrack(track);
else addTracks(track.sub_tracks, (titles || [ ]).concat(track.title));
break;
}
})(release.tracklist);
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 saveToCache(entity, discogsId, mbid) {
bindingsCache[entity][discogsId] = mbid;
GM_setValue('discogs_to_mb_bindings', bindingsCache);
}
function getCachedMBID(entity, discogsId, mbEntity = entity) {
if (!bindingsCache) if (bindingsCache = GM_getValue('discogs_to_mb_bindings'))
console.info('Discogs to MB bindings cache loaded:',
Object.keys(bindingsCache.artist || { }).length, 'artists,',
Object.keys(bindingsCache.label || { }).length, 'labels');
else bindingsCache = {
artist: { 194: '89ad4ac3-39f7-470e-963a-56509c546377' }, // VA
label: {
750: '49b58bdb-3d74-40c6-956a-4c4b46115c9c', // Virgin
1003: '29d7c88f-5200-4418-a683-5c94ea032e38', // BMG
1866: '011d1192-6f65-45bd-85c4-0400dd45693e', // Columbia
5320: 'f18f3b31-8263-4de3-966a-fda317492d3d', // Decca
26126: 'c029628b-6633-439e-bcee-ed02e8a338f7', // EMI
51167: null, // Rough Trade
108701: '7c439400-a83c-48bc-9042-2041711c9599', // Virgin JP
},
};
if (!(entity in bindingsCache)) bindingsCache[entity] = { };
if (!(discogsId in bindingsCache[entity])) return Promise.reject('Not cached');
if (!rxMBID.test(bindingsCache[entity][discogsId])) return Promise.resolve(null);
return globalXHR(`${mbOrigin}/${entity}/${bindingsCache[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 =>
{ notifyFoundMBID(`${entity} ${name} got from cache`, 'sandybrown') });
if (response != bindingsCache[entity][discogsId]) saveToCache(entity, discogsId, response); //return Promise.reject('Location changed');
return response;
});
}
function findMBIDByCommonReleases(entity, discogsId, mbids, mbEntity = entity) {
if (!entity || !(discogsId > 0) || !Array.isArray(mbids)) return Promise.reject('Invalid argument')
if (mbids.length <= 0) return Promise.reject('No MusicBrainz entries');
const getMutualScore = (dcReleases, mbReleases) => [mbReleases, dcReleases].every(Array.isArray) ?
mbReleases.reduce((score, mbRelease) => score + (function() {
if (!mbRelease) return 0; else {
var dcRelease = getDiscogsRels(mbRelease);
dcRelease = dcReleases.filter(dcRelease2 => dcRelease.includes(dcRelease2.id));
console.assert(dcRelease.length < 2, 'Multiple URL relations to release:', mbRelease, dcRelease);
dcRelease = dcRelease.length > 0 && dcRelease[0];
}
if (!dcRelease && !(mbRelease.date && mbRelease.title && (dcRelease = dcReleases.find(function(dcRelease) {
if (!(dcRelease.year > 0) || dcRelease.year != getReleaseYear(mbRelease.date)) return false;
return sameTitleMapper(mbRelease, dcRelease.title, sameStringValues, releaseTitleNorm);
})))) return 0;
if (debugLogging) console.debug('Found matching releases:', dcRelease, mbRelease);
if (dcRelease.role == 'TrackAppearance' || mbRelease.lookupIndex == 1) return 1/2;
return 1;
})(), 0) : 0;
const getDiscogsArtistReleases = (page = 1) =>
dcApiRequest(`${entity}s/${discogsId}/releases`, { page: page, per_page: 500 })
.then(response => response.pagination.page < response.pagination.pages ?
getDiscogsArtistReleases(response.pagination.page + 1)
.then(releases => response.releases.concat(releases)) : response.releases);
return Promise.all([
getDiscogsArtistReleases(),
Promise.all(mbids.map(mbid => mbApiRequest(entity + '/' + mbid, { inc: 'url-rels' }).then(function(release) {
const discogsIds = getDiscogsRels(release);
if (discogsIds.includes(parseInt(discogsId))) return Promise.reject('Matched by relation');
return discogsIds.length > 0 ? null : Promise.reject('No Discogs relations set');
}).catch(reason => Promise.all(({ artist: ['artist', 'track_artist'] }[mbEntity] || [mbEntity])
.map(param => mbLookupById('release', param, mbid).catch(reason => null)))))),
]).then(function([dcReleases, mbReleases]) {
const testFn = RegExp.prototype.test.bind(rxMBID);
const name = dcApiRequest(`${entity + 's'}/${discogsId}`).then(discogsEntry =>
'' + (discogsEntry.name || discogsEntry.title) + '', reason => discogsId);
if (mbReleases.filter(testFn).length == 1) {
const mbid = mbReleases.find(testFn);
discogsName(entity, discogsId).then(name =>
{ notifyFoundMBID(`${entity} ${name} found by having Discogs relative set`, 'salmon') });
return mbid;
}
mbReleases = mbReleases.map(mbResults => Array.isArray(mbResults) ? Array.prototype.concat.apply([ ],
mbResults.map((lookupResults, lookupIndex) => (lookupResults || [ ]).map(lookupResult =>
Object.assign({ lookupIndex: lookupIndex }, lookupResult)))).filter((lookupResult1, index, arr) =>
arr.findIndex(lookupResult2 => lookupResult2.id == lookupResult1.id) == index) : [ ]);
const mutualScores = mbReleases.map(releases => getMutualScore(dcReleases, releases));
const hiScore = Math.max(...mutualScores);
if (!(hiScore >= 1)) return Promise.reject('Not found by common releases');
const hiIndex = mutualScores.indexOf(hiScore);
const dataSize = Math.min(dcReleases.length, mbReleases[hiIndex].length);
if (hiScore * 50 < dataSize) return Promise.reject('Found by common releases with too low match rate');
console.log('Entity binding found by having score %f:\n%s\n%s',
hiScore, [dcOrigin, entity, discogsId].join('/') + '#' + entity,
[mbOrigin, mbEntity, 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, mbEntity, 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();
}
discogsName(entity, discogsId).then(name =>
{ notifyFoundMBID(`${entity} ${name} found by score ${hiScore.toFixed(1)} out of ${dataSize} release(s)`, 'gold') });
saveToCache(entity, discogsId, mbids[hiIndex]);
return mbids[hiIndex];
});
}
function discogsMarkupToMB(source) {
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
&& stripNameSuffix(name.replace(/[\x00-\x1f]+/g, '').trim().replace(/\s+/g, ' '));
const link = (url, caption) => url ? caption ?
`[${encodeURI(url)}|${caption.replace(/[\[\]\|]/g, m => `${m.charCodeAt(0)};`)}]`
: '[' + encodeURI(url) + ']' : '';
return (function(body, replacer) {
if (typeof body != 'string' || typeof replacer != 'function') throw 'Invalid argument';
body = body.replace(/\[([armlu])=([^\[\]\r\n]+)\]/ig,
(match, key, id) => !/^\d+$/.test(id) ? replacer(key, 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;
});
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 =>
findMBIDByCommonReleases(entryTypes[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(entryTypes[entry.key], entry.id, mbEntity(entry.key)).then(function(entries) {
if (entries.length > 1) return ['a', 'l'].includes(entry.key) && idsLookupLimit > 0 ?
findFromMBIDs(entries.map(entry => entry.id)) : Promise.reject('Ambiguity');
discogsEntry.then(discogsEntry => '' + discogsEntry.caption + '', reason => entry.id).then(name =>
{ notifyFoundMBID(`${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) && idsLookupLimit > 0) addMethod(reason => reason != 'Ambiguity (releases)' ?
discogsEntry.then(discogsEntry => mbApiRequest(mbEntity(entry.key), {
query: '"' + discogsEntry.caption + '"',
limit: idsLookupLimit,
})).then(results => findFromMBIDs(results[mbEntity(entry.key) + 's'].map(result => result.id))) : Promise.reject(reason));
addMethod(() => 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, 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);
}).catch(function(reason) {
console.warn(reason);
return source;
}).then(source => [
[/\[url=([^\[\]\r\n]+)\]([^\[\]\r\n]*)\[\/url\]/ig, function(m, url, caption) {
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) {
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, (m, url) => link(url.trim())],
[/\[t=?(\d+)\]/ig, `[${dcOrigin}/help/forums/topic?topic_id=$1]`],
[/\[g=?([^\[\]\r\n]+)\]/ig, `[${dcOrigin}/help/guidelines/$1]`],
[/[ \t]+$/gm, ''], [/(?:\r?\n){2,}/g, '\n\n'],
].reduce((str, substitution) => str.replace(...substitution), source));
}
function detectAlphabet(charSets, formValue) {
if (charSets && formValue) 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))) continue;
formData.set(formValue, key);
return;
} else throw 'Invalid argument';
}
const normTitle = title => title && [].reduce((str, subst) => str.replace(...subst), title);
const literals = { }, lookupIndexes = { artist: { }, label: { } };
const idsLookupLimit = params.mbidsLookup ? GM_getValue('mbid_search_size', 30) : 0;
const discogsName = (entity, discogsId) => entity in lookupIndexes && discogsId in lookupIndexes[entity] ?
Promise.resolve('' + lookupIndexes[entity][discogsId].name + '')
: dcApiRequest(`${entity}s/${discogsId}`).then(discogsEntry =>
'' + (discogsEntry.name || discogsEntry.title) + '', reason => discogsId);
const sameTitleMapper = (entry, title, cmpFn = sameStringValues, normFn = str => str.trim()) =>
entry && title && (entry.title && cmpFn(normFn(entry.title), title = normFn(title))
|| entry.aliases && entry.aliases.some(alias => cmpFn(normFn(alias.name || alias.title), title)));
formData.set('name', normTitle(release.title));
//frequencyAnalysis(literals, release.title);
let media, bindingsCache;
const released = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(release.released);
discogsCountryToIso3166Mapper(release.country).forEach(function(countryCode, countryIndex) {
if (countryCode) formData.set(`events.${countryIndex}.country`, countryCode);
if (released != null) {
const setDate = (index, part) =>
{ formData.set(`events.${countryIndex}.date.${part}`, (index = parseInt(released[index])) > 0 ? index : '') };
setDate(1, 'year'); setDate(2, 'month'); setDate(3, 'day');
}
});
let defaultFormat = 'CD', descriptors = new Set;
if (release.formats) {
for (let format of release.formats) for (let description of getFormatDescriptions(format))
descriptors.add(description);
if (!release.formats.some(format => format.name == 'CD')
&& release.formats.some(format => format.name == 'CDr')) defaultFormat = 'CD-R';
}
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',
}, type => { formData.append('type', type) });
processFormats({
'HDCD': 'HDCD',
'Enhanced': 'Enhanced CD',
'Copy Protected': 'Copy Control CD',
'CD\\+G': 'CD+G',
'DualDisc': 'DualDisc',
[/SHM[ \-]?CD/.source]: 'SHM-CD',
[/(?:BS|Blu-?Spec)[ \-]?CD2?/.source]: 'Blu-spec CD',
[/HQ-?CD/.source]: 'HQCD',
[/DTS[ \-]?CD/.source]: 'DTS CD',
//[/CD[ \-]?ROM/.source]: 'Data CD',
//'CDi': 'Data CD',
//'VCD': 'VCD',
//'SVCD': 'SVCD',
}, format => { defaultFormat = format });
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) {
if (label.name) {
const prefix = 'labels.' + index, name = stripNameSuffix(label.name);
if (rxNoLabel.test(name)) formData.set(prefix + '.mbid', '157afde4-4bf5-4039-8ad2-5a15acc85176');
else {
formData.set(prefix + '.name', name);
if (label.id in lookupIndexes.label) lookupIndexes.label[label.id].prefixes.push(prefix);
else lookupIndexes.label[label.id] = {
name: label.name,
searchName: stripNameSuffix(label.name).replace(rxBareLabel, ''),
prefixes: [prefix],
};
}
}
if (label.catno) formData.set(`labels.${index}.catalog_number`,
rxNoCatno.test(label.catno) ? '[none]' : label.catno);
});
seedArtists(release.artists, release.extrartists);
if (!Array.isArray(cdLengths) || cdLengths.length <= 0) cdLengths = false;
const rxParsingMethods = [
/^()?()?(\S+)$/,
// /^([A-Z]{2,})(?:[\-\ ](\d+))?[\ \-\.]?\b(\S+)$/i,
// /^([A-Z]{2,})?(\d+)?[\ \-\.]?\b(\S+)$/i,
/^(?:([A-Z]{2,})[\ \-\.]?)?(?:(\d+)[\ \-\.])?([A-Z]?\d+[a-z]?)$/i,
];
const layoutMatch = media => Array.isArray(cdLengths) && cdLengths.length > 0 ?
(media = media.filter(isCD)).length == cdLengths.length
&& media.every((medium, mediumIndex) => medium.tracks.length == cdLengths[mediumIndex]) : undefined;
if (rxParsingMethods.some(rx => layoutMatch(media = parseTracks(rx, true))) || !cdLengths
|| rxParsingMethods.some(rx => layoutMatch(media = parseTracks(rx, false))) || cdLengths.length > 0
&& (rxParsingMethods =>
media.filter(isCD).length == 1 && rxParsingMethods.some(function(rx) {
for (let collapseSubtracks of [true, false]) {
let rearrangedMedia = parseTracks(rx, collapseSubtracks);
const cdMedia = rearrangedMedia.filter(isCD);
if (cdMedia.length <= 0) continue;
const cdTracks = Array.prototype.concat.apply([ ], cdMedia.map(medium => medium.tracks));
if (cdTracks.length <= 0) continue;
if (cdTracks.length != cdLengths.reduce((sum, totalTracks) => sum + totalTracks, 0)) continue;
if (layoutMatch(rearrangedMedia = 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(rearrangedMedia.filter(medium => !isCD(medium))))) media = rearrangedMedia;
if (media == rearrangedMedia) return true;
}
return false;
}) || rxParsingMethods.some(function(rx) {
for (let collapseSubtracks of [true, false]) {
let rearrangedMedia = parseTracks(rx, collapseSubtracks);
if (rearrangedMedia.length <= 0) continue;
const cdTracks = Array.prototype.concat.apply([ ], rearrangedMedia.map(medium => medium.tracks));
if (cdTracks.length <= 0) continue;
if (cdTracks.length != cdLengths.reduce((sum, totalTracks) => sum + totalTracks, 0)) continue;
if (layoutMatch(rearrangedMedia = cdLengths.map(function(discNumTracks, discIndex) {
const trackOffset = cdLengths.slice(0, discIndex).reduce((sum, totalTracks) => sum + totalTracks, 0);
const mediaIndex = Math.min(discIndex, rearrangedMedia.length - 1);
return {
format: defaultFormat,
name: rearrangedMedia[mediaIndex].name,
tracks: cdTracks.slice(trackOffset, trackOffset + discNumTracks)
.map((track, index) => Object.assign(track, { number: index + 1 })),
};
}))) media = rearrangedMedia;
if (media == rearrangedMedia) return true;
}
return false;
}))(rxParsingMethods.reverse()) || confirm('Could not find appropriatte tracks mapping to media (' +
media.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) {
formData.set(`mediums.${mediumIndex}.format`, medium.format);
if (medium.name) formData.set(`mediums.${mediumIndex}.name`, normTitle(medium.name));
if (medium.tracks) medium.tracks.forEach(function(track, trackIndex) {
if (track.number) formData.set(`mediums.${mediumIndex}.track.${trackIndex}.number`, track.number);
if (track.name) {
const prefix = str => str ? str + ': ' : '';
const fullTitle = prefix(track.heading) + prefix((track.titles || [ ]).join(' / ')) + track.name;
formData.set(`mediums.${mediumIndex}.track.${trackIndex}.name`, normTitle(fullTitle));
frequencyAnalysis(literals, track.name /*fullTitle*/);
}
if (track.length) formData.set(`mediums.${mediumIndex}.track.${trackIndex}.length`, track.length);
if ((!track.artists || track.artists.length <= 0)
&& (!track.extraartists || track.extraartists.length <= 0)) return;
seedArtists((track.artists && track.artists.length > 0 ? track : release).artists,
(track.extraartists && track.extraartists.length > 0 ? track : release).extraartists,
`mediums.${mediumIndex}.track.${trackIndex}.`);
});
});
}
const charCodes = Object.keys(literals).map(key => parseInt(key));
if (debugLogging) console.debug('charCodes:', charCodes.map(charCode => `${charCode} (0x${charCode.toString(16).toUpperCase()}): ${String.fromCharCode(charCode)}`));
const range = (from, to) => Array.from(Array(to + 1 - from), (_, index) => from + index);
detectAlphabet({
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],
gre: [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],
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],
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],
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],
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],
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],
arm: [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],
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],
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],
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],
}, 'language');
//if (!formData.has('language')) formData.set('language', 'mul');
detectAlphabet({
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)],
}, 'script');
//if (!formData.has('script')) formData.set('script', 'Qaaa');
const packagingMappers = {
[/^Book$/.source]: 'book',
[/^Box$|Box[ \-]?Set/.source]: 'box',
[/(?:Card(?:board)?|Paper) ?Sleeve/.source]: 'cardboard/paper sleeve',
[/Cassette(?: Case)?/.source]: 'cassette case',
[/Clamshell(?: Case)?/.source]: 'clamshell case',
[/Digi ?book/.source]: 'digibook',
[/Digi(?:pac?k|sleeve)(?: (?:Case|Cover))?/.source]: 'digipak',
[/Disc ?box(?: ?Slider)?/.source]: 'discbox slider',
[/Fat ?box(?: Case)?/.source]: 'fatbox',
[/Gatefold(?: Cover)?/.source]: 'gatefold cover',
[/Jewel(?: Case)?/.source]: 'jewel case',
[/^Keep$|Keep Case/.source]: 'keep case',
[/Long ?box(?: Case)?/.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)?/.source]: 'super jewel box',
};
for (let key in packagingMappers) if (new RegExp('\\b(?:' + key + ')\\b', 'i').test(release.notes))
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',
}, status => { formData.set('status', status) });
if ((descriptors = descriptors.map(function(descriptor) {
switch (descriptor) {
case 'Mixed': if (formData.getAll('type').includes('DJ-mix')) return false; else break;
case 'Remastered': if (formData.getAll('type').includes('Compilation')) return false; else break;
}
return descriptor.replace(/\b(\w+)\b/g, (...m) =>
m[1].toUpperCase() == m[1] ? m[1] : m[1].toLowerCase()).trim();
}).filter(Boolean)).length > 0) formData.set('comment', descriptors.join(', '));
//else formData.delete('comment');
if (!formData.has('status')) formData.set('status', 'official');
let annotation = [
release.notes && [
[/(?:\r?\n)*^(?:Total (?:(?:Running|Playing) )?(?:Time|Length|Duration)|TT):? +(?:(?:\d+:)+\d+|(?:\d+['"] ?){2})$/gmi, ''],
].reduce((annotation, subst) => annotation.replace(...subst), release.notes).trim(),
release.identifiers && release.identifiers
.filter(identifier => !['Barcode', 'ASIN'].includes(identifier.type))
.map(identifier => identifier.type + ': ' + identifier.value).join('\n'),
].filter(Boolean);
annotation = annotation.length > 0 ? discogsMarkupToMB(annotation.join('\n\n'))
.then(annotation => { formData.set('annotation', annotation) }) : undefined;
let urlRelIndex = -1;
addUrlRef(dcOrigin + '/release/' + release.id, 76);
if (release.identifiers) for (let identifier of release.identifiers) switch (identifier.type) {
case 'Barcode': formData.set('barcode', identifier.value.replace(/\D+/g, '')); break;
case 'ASIN': addUrlRef('https://www.amazon.com/dp/' + identifier.value, 77); break;
}
const artistCredits = { };
if (Array.isArray(release.extraartists) && release.extraartists.length > 0) {
for (let artistCredit of release.extraartists) if (!artistCredit.tracks)
for (let role of artistCredit.role.split(',').map(role => role.trim()).filter(Boolean)) {
if (!(role in artistCredits)) artistCredits[role] = new Set;
artistCredits[role].add(artistCredit);
}
for (let role in artistCredits) if (['DJ Mix', 'Compiled By'].includes(role))
for (let artistCredit of artistCredits[role].values())
if (!(artistCredit.id in lookupIndexes.artist)) lookupIndexes.artist[artistCredit.id] = {
name: artistCredit.name,
searchName: stripNameSuffix(artistCredit.name),
prefixes: [role],
}; else lookupIndexes.artist[artistCredit.id].prefixes.push(role);
if ('DJ Mix' in artistCredits && formData.getAll('type').includes('Compilation'))
formData.append('type', 'DJ-mix');
}
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from Discogs release id ${release.id}`).trimLeft());
const rgLookupWorkers = [ ];
if (params.mbidsLookup && params.rgLookup && !formData.has('release_group') && release.master_id > 0)
rgLookupWorkers.push(findDiscogsRelatives('master', release.master_id, 'release-group').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));
return Promise.all([
getSessions(torrentId).catch(reason => null).then(function(sessions) {
function recordingsLookup(medium, track, mbidLookupFn) {
if (!track) throw 'Invalid argument';
if (!track.name) 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(media)) 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 bracketStripper = [/(?:\s+(?:\(.+\)|\[.+\]))+$/, ''];
const artists = (track.artists && track.artists.length > 0 ? track : release).artists;
console.assert(artists && artists.length > 0, track);
return artists && artists.length > 0 ? 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 = mbidLookupFn && mbidLookupFn('artist', artist.id);
return arid ? 'arid:' + arid : 'artistname:"' + stripNameSuffix(artist.name) + '"';
});
fields.push(OR([track.name, track.name.replace(...bracketStripper)],
recording => 'recording:"' + track.name + '"'));
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 (['Live', 'Interview', 'Demo'/*, 'DJ-mix'*/].some(secondaryType => recording.releases
&& recording.releases.length > 0 && (formData.getAll('type').includes(secondaryType) ?
!recording.releases.some(release => !('release-group' in release)
|| 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType))
: recording.releases.some(release => 'release-group' in release
&& 'secondary-types' in release['release-group']
&& release['release-group']['secondary-types'].includes(secondaryType)))))
return false;
if (!Array.isArray(recording['artist-credit']) || !artists.every(function(artist) {
const arid = mbidLookupFn && 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 lengthMapper = 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(lengthMapper)) 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(lengthMapper)) 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(lengthMapper),
].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 = { };
const matchnameVariant = (artist, nameVariant) => nameVariant
&& (artist.name && sameStringValues(stripNameSuffix(artist.name), nameVariant)
|| artist.namevariations && artist.namevariations.some(anv => sameStringValues(anv, nameVariant)));
if (idsLookupLimit > 0) for (let medium of media) if ('tracks' in medium)
for (let track of medium.tracks) (function addArtistLookups(root) {
if (Array.isArray(root.artists)) for (let artist of root.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));
}
})(Array.isArray(track.artists) && track.artists.length > 0 ? track : release);
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();
});
return mbid;
}
const printArtistMBIDs = mbids => Object.keys(mbids).map(mbid =>
[mbOrigin, 'artist', mbid, 'recordings'].join('/') + ' => ' + mbids[mbid]);
let promise = getCachedMBID(entity, discogsId);
promise = promise.catch(() => 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 idsLookupLimit > 0 ?
findMBIDByCommonReleases(entity, discogsId, entries.map(entry => entry.id))
: Promise.reject('Ambiguity');
notifyFoundMBID(`${entity} ${lookupIndexes[entity][discogsId].name} found by having Discogs relative set`, 'salmon');
saveToCache(entity, discogsId, entries[0].id);
return entries[0].id;
}));
if (idsLookupLimit > 0) promise = promise.catch(reason => reason != 'Ambiguity (releases)' ? mbApiRequest(entity, {
query: '"' + (lookupIndexes[entity][discogsId].searchName || lookupIndexes[entity][discogsId].name) + '"',
limit: idsLookupLimit,
}).then(results => findMBIDByCommonReleases(entity, discogsId, results[entity + 's']
.map(result => result.id))) : Promise.reject(reason));
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();
}
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, entity, discogsId].join('/') + '#' + entity, [mbOrigin, entity, mbid, 'releases'].join('/'));
notifyFoundMBID(`${entity} ${lookupIndexes[entity][discogsId].name} found by match with ${hiValue} existing recordings`, 'hotpink');
saveToCache(entity, discogsId, mbid);
return mbid;
}));
return promise.then(checkMBID).catch(reason => null);
})))).then(function(lookupResults) {
function findMBID(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;
}
function findExistingRecordings() {
if (params.recordingsLookup <= 0) return; else if (!(params.recordingsLookup > 1)) {
if (formData.has('release_group')) return;
if (['DJ-mix', 'Remix', 'Live'].some(secondaryType => formData.getAll('type').includes(secondaryType))
|| descriptors.some(RegExp.prototype.test.bind(/\b(?:(?:re)?mix(?:ed)?|mixtape)\b/i))) return;
}
return Promise.all(media.map(function(medium, mediumIndex) {
if (!medium || !Array.isArray(medium.tracks) || canContainVideo(medium)) return;
return Promise.all(medium.tracks.map((track, trackIndex) =>
recordingsLookup(medium, track, findMBID).then(function(recordings) {
if ((recordings = recordings.filter(recording =>
!/\b(?:(?:re)?mix|rework)/i.test(recording.disambiguation))).length <= 0)
return Promise.reject('No matches');
formData.set(`mediums.${mediumIndex}.track.${trackIndex}.recording`, recordings[0].id);
let notifyText = `recording ${track.name} found`, firstRelease = [ ];
if (recordingDate(recordings[0])) firstRelease.push('' +
getReleaseYear(recordingDate(recordings[0])) + '');
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 ' + (releaseType ? releaseType + ' ' + release.title : release.title) + '');
}
}
if (firstRelease.length > 0) notifyText += ` (first released ${firstRelease.join(' ')})`;
notifyFoundMBID(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)*/ })));
}));
}
Object.keys(lookupIndexes).forEach(function(entity, ndx1) {
Object.keys(lookupIndexes[entity]).forEach(function(discogsId, ndx2) {
if (lookupResults[ndx1][ndx2] == null) return;
for (let prefix of lookupIndexes[entity][discogsId].prefixes)
if (prefix in artistCredits)
console.info('MBID for %s %s:', prefix, lookupIndexes[entity][discogsId].name,
lookupResults[ndx1][ndx2]);
else
formData.set(prefix + '.mbid', lookupResults[ndx1][ndx2]);
});
});
if (!formData.has('release_group') && Array.isArray(release.artists)
&& release.artists.length > 0 && release.artists[0].id != 194) {
const mbid = findMBID('artist', release.artists[0].id);
if (mbid && params.rgLookup) rgLookupWorkers.push(mbLookupById('release-group', 'artist', mbid).then(function(releaseGroups) {
function rgFilter(releaseGroup, strictType = false, strictName = true) {
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);
}
let filtered = releaseGroups.filter(releaseGroup => rgFilter(releaseGroup, false, true));
if (filtered > 1)
filtered = releaseGroups.filter(releaseGroup => rgFilter(releaseGroup, true, true));
else if (filtered <= 0)
filtered = releaseGroups.filter(releaseGroup => rgFilter(releaseGroup, false, false));
if (filtered > 1)
filtered = releaseGroups.filter(releaseGroup => rgFilter(releaseGroup, true, false));
if (filtered.length == 1) return filtered[0];
}, console.error));
}
return Promise.all(rgLookupWorkers).then(function(releaseGroups) {
const releaseGroup = releaseGroups.find(Boolean);
if (releaseGroup) formData.set('release_group', releaseGroup.id); else return false;
let notify = `release group ${releaseGroup.name || releaseGroup.title}`;
if (releaseGroup['first-release-date'])
notify += ` (${getReleaseYear(releaseGroup['first-release-date'])})`;
notify += ` found by ${'relations' in releaseGroup ? 'unique name match' : 'known URL relation'}`;
notifyFoundMBID(notify, 'goldenrod');
return true;
}).then(findExistingRecordings);
});
}),
annotation,
]).then(() => formData);
});
}
function seedNewRelease(formData) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
// if (!formData.has('language')) formData.set('language', 'eng');
if (formData.has('language')) formData.set('script', {
eng: 'Latn', deu: 'Latn', spa: 'Latn', fra: 'Latn', ita: 'Latn', swe: 'Latn', nor: 'Latn', fin: 'Latn',
por: 'Latn', nld: 'Latn', pol: 'Latn', ces: 'Latn', slk: 'Latn', slv: 'Latn', hrv: 'Latn', srp: 'Latn',
hun: 'Latn', tur: 'Latn', dan: 'Latn', ltz: 'Latn', ron: 'Latn', est: 'Latn', lav: 'Latn', isl: 'Latn',
gre: 'Grek', ell: 'Grek', rus: 'Cyrl', bul: 'Cyrl', mkd: 'Cyrl', heb: 'Hebr', ara: 'Arab', arm: 'Armn',
jpn: 'Jpan', zho: 'Hant', kor: 'Kore', tha: 'Thai',
}[(formData.get('language') || '').toLowerCase()] || 'Latn');
formData.set('edit_note', ((formData.get('edit_note') || '') + '\nSeeded by ' + scriptSignature).trimLeft());
if (mbSeedNew < 2) formData.set('make_votable', 1);
const form = document.createElement('form');
[form.method, form.action, form.target, form.hidden] = ['POST', mbOrigin + '/release/add', '_blank', true];
form.append(...Array.from(formData, entry => 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
: 'Media 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);
}
}
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' : 'media 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(function(responses) {
GM_openInTab(`${mbOrigin}/release/${mbId}/discids`, false);
return true;
});
}).catch(reason => { alert(reason + '\n\nAttach by hand'); attachByHand() }) : attachByHand());
}, alert);
function attachToMBIcon(callback, style, tooltip, tooltipster) {
//