// ==UserScript==
// @name [GMT] Edition lookup by CD TOC
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.15.13
// @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 api.discogs.com
// @connect www.discogs.com
// @connect db.cuetools.net
// @connect db.cue.tools
// @connect gnudb.org
// @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 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
// ==/UserScript==
{
'use strict';
const requestsCache = new Map, mbRequestsCache = new Map;
let mbLastRequest = null, noEditPerms = document.getElementById('nav_userclass');
noEditPerms = noEditPerms != null && ['User', 'Member', 'Power User'].includes(noEditPerms.textContent.trim());
const [mbOrigin, dcOrigin] = ['https://musicbrainz.org', 'https://www.discogs.com'];
function setTooltip(elem, tooltip, params) {
if (!(elem instanceof HTMLElement)) throw 'Invalid argument';
if (typeof jQuery.fn.tooltipster == 'function') {
if (tooltip) tooltip = tooltip.replace(/\r?\n/g, '<br>')
if ($(elem).data('plugin_tooltipster'))
if (tooltip) $(elem).tooltipster('update', tooltip).tooltipster('enable');
else $(elem).tooltipster('disable');
else if (tooltip) $(elem).tooltipster(Object.assign(params || { }, { content: tooltip }));
} else if (tooltip) elem.title = tooltip; else elem.removeAttribute('title');
}
function getTorrentId(tr) {
if (!(tr instanceof HTMLElement)) throw 'Invalid argument';
if ((tr = tr.querySelector('a.button_pl')) != null
&& (tr = parseInt(new URLSearchParams(tr.search).get('torrentid'))) > 0) return tr;
}
function 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, 100, reqCounter);
const now = Date.now();
if (now <= mbLastRequest + 1000) return setTimeout(request, mbLastRequest + 1000 - now, reqCounter);
mbLastRequest = Infinity;
globalXHR(url, { responseType: 'json' }).then(function({response}) {
mbLastRequest = Date.now();
resolve(response);
}, function(reason) {
mbLastRequest = Date.now();
if (/^HTTP error (?:429|430)\b/.test(reason)) return setTimeout(request, 1000, reqCounter + 1);
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;
})();
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);
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) 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;
}
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 rxSessionHeader = '^(?:' + [
'(?: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: ',
].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);
return 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`;
console.assert(msfToSector(tocEntry[2]) == parseInt(tocEntry[12]));
console.assert(msfToSector(tocEntry[7]) == parseInt(tocEntry[13]) + 1 - parseInt(tocEntry[12]));
return {
trackNumber: parseInt(tocEntry[1]),
startSector: parseInt(tocEntry[12]),
endSector: parseInt(tocEntry[13]),
};
})).length > 0 ? tocEntries : null;
}
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)) {
const rxRipperSignatures = '(?:(?:' + [
'Exact Audio Copy V', 'X Lossless Decoder version ',
'CUERipper v', 'EZ CD Audio Converter ', 'Log created by: whipper ',
].join('|') + ')\\d+)';
if (!detectVolumes) {
const rxStackedLog = new RegExp('^[\\S\\s]*(?:\\r?\\n)+(?=' + rxRipperSignatures + ')');
return (logFiles = Array.prototype.map.call(logFiles, (logFile =>
rxStackedLog.test(logFile) ? logFile.replace(rxStackedLog, '') : logFile))
.filter(RegExp.prototype.test.bind(new RegExp('^(?:' + rxRipperSignatures + '|EAC\\b)')))).length > 0 ?
logFiles : null;
}
if ((logFiles = Array.prototype.map.call(logFiles, function(logFile) {
const rxSessionsIndexer = new RegExp('^' + rxRipperSignatures, 'gm');
let indexes = [ ], match;
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(rxSessionHeader + '(?:\\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 ? Array.from(sessions.values()) : null;
}
function getSessions(torrentId) {
if (!(torrentId > 0)) throw 'Invalid argument';
if (requestsCache.has(torrentId)) return requestsCache.get(torrentId);
// let request = queryAjaxAPICached('torrent', { id: torrentId }).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 }))
.then(document => Array.from(document.body.querySelectorAll(':scope > blockquote > pre:first-child'),
pre => pre.textContent));
requestsCache.set(torrentId, request = request.then(getUniqueSessions).then(sessions =>
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 && index == tocEntries.length - 2 ? 1 : -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`;
const layoutType = getlayoutType(tocEntries);
if (layoutType == 1) tocEntries.pop(); // ditch data track for CD Extra
else if (layoutType != 0) console.warn('Disc %d unknown layout type', volumeNdx + 1);
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 bareId = str => str ? str.trim().toLowerCase()
.replace(/^(?:Not On Label|No label|\[no label\]|None|\[none\]|Self[\s\-]?Released)(?:\s*\(.+\))?$|(?:\s+\b(?:Record(?:ing)?s)\b|,?\s+(?:Ltd|Inc|Co)\.?)+$/ig, '')
.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 => /^(?:Not On Label|No label|\[no label\]|None|\[none\])(?:\s*\(.+\))?$|\b(?:Self[\s\-]?Released)\b/i.test(label) ? 'self-released' : label).filter(Boolean).join(' / ');
payload.remaster_catalogue_number = editionInfo.map(label => label.catNo).filter(uniqueValues)
.map(catNo => !/^(?:\[none\]|None)$/i.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.color = evt.type == 'mouseenter' ? 'orange' : 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';
container = document.createElement('DIV');
container.className = 'toc-lookup-tables';
container.style = 'margin: 10pt 0; padding: 0; display: flex; flex-flow: column; row-gap: 10pt;';
elem.after(container);
}
(elem = document.createElement('DIV')).append(...elems);
container.append(elem);
}
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 = (height = '1em', color = '#f00') => minifyHTML(`
<svg height="${height}" version="1.1" viewBox="0 0 256 256">
<circle fill="${color}" cx="128" cy="128" r="128" />
<path fill="white" d="M197.7 83.38l-1.78 1.78 -1.79 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.75 1.75 1.75 1.75 1.79 1.79 1.78 1.78 1.79 1.79 1.79 1.79 1.78 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.78 1.79 1.79 1.79 1.79 1.78 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.78c6.58,6.58 -18.5,31.66 -25.08,25.08l-44.62 -44.62 -1.75 1.75 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.79 -1.78 1.78 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.79 1.78 -1.78 1.79 -1.79 1.79 -1.78 1.78c-6.58,6.58 -31.66,-18.5 -25.08,-25.08l44.62 -44.62 -44.62 -44.62c-6.58,-6.58 18.5,-31.66 25.08,-25.08l1.78 1.78 1.79 1.79 1.79 1.79 1.78 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.78 1.79 1.79 1.79 1.79 1.78 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.79 1.79 1.78 1.78 1.79 1.79 1.79 1.79 1.78 1.78 1.79 1.79 1.75 1.75 44.62 -44.62c6.58,-6.58 31.66,18.5 25.08,25.08z" />
</svg>`);
const svgCheckmark = (height = '1em', color = '#0c0') => minifyHTML(`
<svg height="${height}" version="1.1" viewBox="0 0 4120.39 4120.39">
<circle fill="${color}" cx="2060.2" cy="2060.2" r="2060.2" />
<path fill="white" d="M1849.38 3060.71c-131.17,0 -356.12,-267.24 -440.32,-351.45 -408.56,-408.55 -468.78,-397.75 -282.81,-581.74 151.52,-149.91 136.02,-195.31 420.15,88.91 66.71,66.73 168.48,183.48 238.34,230.26 60.59,-40.58 648.52,-923.59 736.78,-1056.81 262.36,-396.02 237.77,-402.28 515.29,-195.27 150.69,112.4 -16.43,237.96 -237.31,570.2l-749.75 1108.47c-44.39,66.71 -104.04,187.43 -200.37,187.43z" />
</svg>`);
const svgQuestionmark = (height = '1em', color = '#fc0') => minifyHTML(`
<svg height="${height}" version="1.1" viewBox="0 0 256 256">
<circle fill="${color}" cx="128" cy="128" r="128" />
<path fill="white" d="M103.92 165.09c-0.84,-2.13 -1.46,-4.52 -1.92,-7.15 -0.46,-2.68 -0.67,-5.19 -0.67,-7.54 0,-3.76 0.37,-7.19 1.09,-10.29 0.75,-3.14 1.84,-6.06 3.3,-8.78 1.51,-2.72 3.35,-5.36 5.52,-7.83 2.22,-2.51 4.81,-4.98 7.74,-7.45 3.1,-2.59 5.82,-5.02 8.16,-7.28 2.3,-2.25 4.31,-4.47 5.94,-6.73 1.63,-2.26 2.85,-4.6 3.68,-6.99 0.8,-2.42 1.22,-5.1 1.22,-8.03 0,-2.55 -0.46,-4.9 -1.34,-7.07 -0.92,-2.14 -2.18,-4.02 -3.89,-5.57 -1.67,-1.54 -3.68,-2.76 -6.11,-3.68 -2.43,-0.88 -5.1,-1.34 -8.03,-1.34 -6.36,0 -12.97,1.34 -19.88,3.98 -6.86,2.68 -13.34,6.69 -19.45,12.09l0 -36.9c6.27,-3.77 13.14,-6.57 20.58,-8.45 7.45,-1.89 15.11,-2.85 23.06,-2.85 7.57,0 14.64,0.84 21.17,2.55 6.57,1.68 12.26,4.31 17.11,7.91 4.85,3.56 8.66,8.16 11.42,13.77 2.72,5.6 4.1,12.34 4.1,20.16 0,4.98 -0.58,9.5 -1.71,13.56 -1.18,4.01 -2.85,7.86 -5.03,11.46 -2.21,3.6 -4.97,7.03 -8.24,10.34 -3.26,3.3 -7.03,6.73 -11.25,10.25 -2.89,2.38 -5.4,4.56 -7.53,6.61 -2.18,2.05 -3.98,4.05 -5.4,6.06 -1.42,2.01 -2.51,4.14 -3.26,6.36 -0.71,2.26 -1.09,4.81 -1.09,7.7 0,1.93 0.25,3.93 0.79,5.98 0.51,2.05 1.26,3.77 2.14,5.15l-32.22 0zm17.87 53.68c-6.53,0 -11.97,-1.93 -16.28,-5.86 -4.35,-4.1 -6.53,-8.91 -6.53,-14.47 0,-5.74 2.18,-10.51 6.53,-14.36 4.31,-3.84 9.75,-5.73 16.28,-5.73 6.48,0 11.8,1.89 16.06,5.73 4.27,3.77 6.36,8.54 6.36,14.36 0,5.89 -2.05,10.75 -6.23,14.6 -4.27,3.8 -9.67,5.73 -16.19,5.73z" />
</svg>`);
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 = document.createElement('SPAN'), a = document.createElement('A');
span.className = 'brackets';
span.dataset.torrentId = torrentId;
span.style = 'display: inline-flex; flex-flow: row; column-gap: 5px; color: initial;';
if (edition != null) span.dataset.edition = edition;
if (incompleteEdition.test(editionInfo)) span.dataset.editionInfoMissing = true;
a.textContent = caption;
a.className = 'toc-lookup';
a.href = '#';
a.onclick = evt => { callback(evt); return false };
if (tooltip) setTooltip(a, tooltip);
span.append(a);
container.append(span);
}
function addClickableIcon(svg, clickHandler, dropHandler, className, style, tooltip, tooltipster = false) {
if (!svg || typeof clickHandler != 'function') throw 'Invalid argument';
const span = document.createElement('SPAN');
span.innerHTML = svg;
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);
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>${title}</title>`);
}
function mbFindEditionInfoInAnnotation(elem, mbId) {
if (!mbId || !(elem instanceof HTMLElement)) throw 'Invalid argument';
return mbApiRequest('annotation', { query: `entity:${mbId} AND type:release` }).then(function(response) {
if (response.count <= 0 || (response = response.annotations.filter(function(annotation) {
console.assert(annotation.type == 'release' && annotation.entity == mbId, 'Unexpected annotation for MBID %s:', mbId, annotation);
return /\b(?:Label|Catalog|Cat(?:alog(?:ue)?)?\s*(?:[#№]|Num(?:ber|\.?)|(?:No|Nr)\.?))\s*:/i.test(annotation.text);
})).length <= 0) return Promise.reject('No edition info in annotation');
const a = document.createElement('A');
[a.href, a.target] = [mbOrigin + '/release/' + mbId, '_blank'];
[a.textContent, a.style] = ['by annotation', 'font-style: italic; ' + noLinkDecoration];
a.title = response.map(annotation => annotation.text).join('\n');
elem.append(a);
});
}
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;';
linkBox.append(' ', container);
const releaseEventToHtml = (country, date) => [
country && `<span class="country"><img src="http://s3.cuetools.net/flags/${country.toLowerCase()}.png" height="9" title="${country.toUpperCase()}" onerror="this.replaceWith(this.title)" /></span>`,
date && `<span class="date">${date}</span>`,
].filter(Boolean).join(' ');
const releaseToHtml = (release, country = 'country', date = 'date') =>
release ? releaseEventToHtml(release[country], release[date]) : '';
const stripNameSuffix = name => name && name.replace(/\s+\(\d+\)$/, '');
const noLinkDecoration = 'background: none !important; padding: 0 !important;';
const linkHTML = (url, caption, cls) => `<a href="${url}" target="_blank" class="${cls || ''}" style="${noLinkDecoration}">${caption}</a>`;
const svgBulletHTML = color => `<svg style="margin-right: 2pt;" viewBox="0 0 10 10" height="0.8em"><circle fill="${color || ''}" cx="5" cy="5" r="5"></circle></svg>`;
addLookup('MusicBrainz', function(evt) {
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('https://musicbrainz.org/otherlookup/freedbid?other-lookup.freedbid=' + discId, false);
}).catch(function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
} else if (Boolean(target.dataset.haveResponse)) {
if ('ids' in target.dataset) for (let id of JSON.parse(target.dataset.ids).reverse())
GM_openInTab('https://musicbrainz.org/release/' + id, false);
// GM_openInTab(`${mbOrigin}/cdtoc/${evt.shiftKey ? 'attach?toc=' + JSON.parse(target.dataset.toc).join(' ')
// : target.dataset.discId}`, false);
} else {
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.length < 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 = expr.trim()) || !entity) return null;
let mbId = rxMBID.exec(expr);
if (mbId != null) return mbId[1]; else 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] : null;
}
function discogsIdExtractor(expr, entity) {
if (!expr || !(expr = expr.trim()) || !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 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) => 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) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
formData.set('name', torrent.group.name);
if (torrent.torrent.remasterTitle) formData.set('comment', 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'); break;
}
}
if (torrent.group.releaseType == 7)
formData.set('artist_credit.names.0.mbid', '89ad4ac3-39f7-470e-963a-56509c546377');
else if (torrent.group.musicInfo) {
let artistIndex = -1;
for (let role of ['dj', 'artists']) if (artistIndex < 0) torrent.group.musicInfo[role].forEach(function(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 ? ', ' : ' & ');
});
}
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) if (/^(?:(?:Self[- ]?Released|Not On Label|no label|\[no label\]|None|\[none\])$|iMD\b|Independ[ae]nt\b)/i.test(torrent.torrent.remasterRecordLabel))
formData.set('labels.0.mbid', '157afde4-4bf5-4039-8ad2-5a15acc85176');
else formData.set('labels.0.name', torrent.torrent.remasterRecordLabel);
if (torrent.torrent.remasterCatalogueNumber) {
formData.set('labels.0.catalog_number', torrent.torrent.remasterCatalogueNumber);
let barcode = torrent.torrent.remasterCatalogueNumber.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) {
if (Array.isArray(sessions) && sessions.length > 0 && GM_getValue('mb_seed_with_fingerprints', false))
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, discogsId, cdLengths, idsLookupLimit = GM_getValue('mbid_search_size', 30)) {
if (!formData || typeof formData != 'object') throw 'Invalid argument';
if (discogsId < 0) [idsLookupLimit, discogsId] = [0, -discogsId];
return discogsId > 0 ? dcApiRequest('releases/' + discogsId).then(function(release) {
function seedArtists(root, prefix) {
if (root && Array.isArray(root)) root.forEach(function(artist, index) {
const creditPrefix = `${prefix || ''}artist_credit.names.${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 (artist.join) formData.set(`${creditPrefix}.join_phrase`, formattedJoinPhrase(artist.join));
else formData.delete(`${creditPrefix}.join_phrase`);
if (!(artist.id in lookupIndexes.artist)) lookupIndexes.artist[artist.id] = {
name: name, prefixes: [creditPrefix],
}; else lookupIndexes.artist[artist.id].prefixes.push(creditPrefix);
});
}
function addUrlRef(url, linkType) {
formData.set(`urls.${++urlRelIndex}.url`, url);
if (linkType != undefined) formData.set(`urls.${urlRelIndex}.link_type`, linkType);
}
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;
const literals = { }, lookupIndexes = { artist: { }, label: { } };
formData.set('name', release.title);
frequencyAnalysis(literals, release.title);
let released = new Date(release.released), media;
if (isNaN(released)) released = release.year;
(release.country ? {
'US': ['US'], 'UK': ['GB'], 'Germany': ['DE'], 'France': ['FR'], 'Japan': ['JP'], 'Italy': ['IT'],
'Europe': ['XE'], 'Canada': ['CA'], 'Netherlands': ['NL'], 'Unknown': ['??'], 'Spain': ['ES'],
'Australia': ['AU'], 'Russia': ['RU'], 'Sweden': ['SE'], 'Brazil': ['BR'], 'Belgium': ['BE'],
'Greece': ['GR'], 'Poland': ['PL'], 'Mexico': ['MX'], 'Finland': ['FI'], 'Jamaica': ['JM'],
'Switzerland': ['CH'], 'USSR': ['RU'], 'Denmark': ['DK'], 'Argentina': ['AR'], 'Portugal': ['PT'],
'Norway': ['NO'], 'Austria': ['AT'], 'UK & Europe': ['GB', 'XE'], 'New Zealand': ['NZ'],
'South Africa': ['ZA'], 'Yugoslavia': ['YU'], 'Hungary': ['HU'], 'Colombia': ['CO'],
'USA & Canada': ['US', 'CA'], 'Ukraine': ['UA'], 'Turkey': ['TR'], 'India': ['IN'],
'Czech Republic': ['CZ'], 'Czechoslovakia': ['CS'], 'Venezuela': ['VE'], 'Ireland': ['IE'],
'Romania': ['RO'], 'Indonesia': ['ID'], 'Taiwan': ['TW'], 'Chile': ['CL'], 'Peru': ['PE'],
'South Korea': ['KR'], 'Worldwide': ['XW'], 'Israel': ['IL'], 'Bulgaria': ['BG'],
'Thailand': ['TH'], 'Malaysia': ['MY'], 'Scandinavia': ['SE', 'NO', 'FI'],
'German Democratic Republic (GDR)': ['DE'], 'China': ['CN'], 'Croatia': ['HR'],
'Hong Kong': ['HK'], 'Philippines': ['PH'], 'Serbia': ['RS'], 'Ecuador': ['EC'],
'Lithuania': ['LT'], 'UK, Europe & US': ['GB', 'XE', 'US'], 'East Timor': ['TL'],
'Germany, Austria, & Switzerland': ['DE', 'AT', 'CH'], 'USA & Europe': ['US', 'XE'],
'Singapore': ['SG'], 'Slovenia': ['SI'], 'Slovakia': ['SK'], 'Uruguay': ['UY'],
'Australasia': ['AU'], 'Australia & New Zealand': ['AU', 'NZ'], 'Iceland': ['IS'],
'Bolivia': ['BO'], 'UK & Ireland': ['GB', 'IE'], 'Nigeria': ['NG'], 'Estonia': ['EE'],
'USA, Canada & Europe': ['US', 'CA', 'XE'], 'Benelux': ['BE', 'NL', 'LU'], 'Panama': ['PA'],
'UK & US': ['GB', 'US'], 'Pakistan': ['PK'], 'Lebanon': ['LB'], 'Egypt': ['EG'], 'Cuba': ['CU'],
'Costa Rica': ['CR'], 'Latvia': ['LV'], 'Puerto Rico': ['PR'], 'Kenya': ['KE'], 'Iran': ['IR'],
'Belarus': ['BY'], 'Morocco': ['MA'], 'Guatemala': ['GT'], 'Saudi Arabia': ['SA'],
'Trinidad & Tobago': ['TT'], 'Barbados': ['BB'], 'USA, Canada & UK': ['US', 'CA', 'GB'],
'Luxembourg': ['LU'], 'Czech Republic & Slovakia': ['CZ', 'SK'], 'Bosnia & Herzegovina': ['BA'],
'Macedonia': ['MK'], 'Madagascar': ['MG'], 'Ghana': ['GH'], 'Zimbabwe': ['ZW'],
'El Salvador': ['SV'], 'North America (inc Mexico)': ['US', 'CA', 'MX'], 'Algeria': ['DZ'],
'Singapore, Malaysia & Hong Kong': ['SG', 'MY', 'HK'], 'Dominican Republic': ['DO'],
'France & Benelux': ['FR', 'BE', 'NL', 'LU'], 'Ivory Coast': ['CI'], 'Tunisia': ['TN'],
'Reunion': ['RE'], 'Angola': ['AO'], 'Serbia and Montenegro': ['RS', 'ME'], 'Georgia': ['GE'],
'United Arab Emirates': ['AE'], 'Congo, Democratic Republic of the': ['CD'],
'Germany & Switzerland': ['DE', 'CH'], 'Malta': ['MT'], 'Mozambique': ['MZ'], 'Cyprus': ['CY'],
'Mauritius': ['MU'], 'Azerbaijan': ['AZ'], 'Zambia': ['ZM'], 'Kazakhstan': ['KZ'],
'Nicaragua': ['NI'], 'Syria': ['SY'], 'Senegal': ['SN'], 'Paraguay': ['PY'], 'Guadeloupe': ['GP'],
'UK & France': ['GB', 'FR'], 'Vietnam': ['VN'], 'UK, Europe & Japan': ['GB', 'XE', 'JP'],
'Bahamas, The': ['BS'], 'Ethiopia': ['ET'], 'Suriname': ['SR'], 'Haiti': ['HT'],
'Singapore & Malaysia': ['SG', 'MY'], 'Moldova, Republic of': ['MD'], 'Faroe Islands': ['FO'],
'Cameroon': ['CM'], 'South Vietnam': ['VN'], 'Uzbekistan': ['UZ'], 'South America': ['ZA'],
'Albania': ['AL'], 'Honduras': ['HN'], 'Martinique': ['MQ'], 'Benin': ['BJ'], 'Kuwait': ['KW'],
'Sri Lanka': ['LK'], 'Andorra': ['AD'], 'Liechtenstein': ['LI'], 'Curaçao': ['CW'], 'Mali': ['ML'],
'Guinea': ['GN'], 'Congo, Republic of the': ['CG'], 'Sudan': ['SD'], 'Mongolia': ['MN'],
'Nepal': ['NP'], 'French Polynesia': ['PF'], 'Greenland': ['GL'], 'Uganda': ['UG'],
'Bangladesh': ['BD'], 'Armenia': ['AM'], 'North Korea': ['KP'], 'Bermuda': ['BM'], 'Iraq': ['IQ'],
'Seychelles': ['SC'], 'Cambodia': ['KH'], 'Guyana': ['GY'], 'Tanzania': ['TZ'], 'Bahrain': ['BH'],
'Jordan': ['JO'], 'Libya': ['LY'], 'Montenegro': ['ME'], 'Gabon': ['GA'], 'Togo': ['TG'],
'Afghanistan': ['AF'], 'Yemen': ['YE'], 'Cayman Islands': ['KY'], 'Monaco': ['MC'],
'Papua New Guinea': ['PG'], 'Belize': ['BZ'], 'Fiji': ['FJ'], 'UK & Germany': ['UK', 'DE'],
'New Caledonia': ['NC'], 'Protectorate of Bohemia and Moravia': ['CS'],
'UK, Europe & Israel': ['GB', 'XE', 'IL'], 'French Guiana': ['GF'], 'Laos': ['LA'],
'Aruba': ['AW'], 'Dominica': ['DM'], 'San Marino': ['SM'], 'Kyrgyzstan': ['KG'],
'Burkina Faso': ['BF'], 'Turkmenistan': ['TM'], 'Namibia': ['NA'], 'Sierra Leone': ['SL'],
'Marshall Islands': ['MH'], 'Botswana': ['BW'], 'Eritrea': ['ER'], 'Saint Kitts and Nevis': ['KN'],
'Guernsey': ['GG'], 'Jersey': ['JE'], 'Guam': ['GU'], 'Central African Republic': ['CF'],
'Grenada': ['GD'], 'Qatar': ['QA'], 'Somalia': ['SO'], 'Liberia': ['LR'], 'Sint Maarten': ['SX'],
'Saint Lucia': ['LC'], 'Lesotho': ['LS'], 'Maldives': ['MV'], 'Bhutan': ['BT'], 'Niger': ['NE'],
'Saint Vincent and the Grenadines': ['VC'], 'Malawi': ['MW'], 'Guinea-Bissau': ['GW'],
'Palau': ['PW'], 'Comoros': ['KM'], 'Gibraltar': ['GI'], 'Cook Islands': ['CK'],
'Mauritania': ['MR'], 'Tajikistan': ['TJ'], 'Rwanda': ['RW'], 'Samoa': ['WS'], 'Oman': ['OM'],
'Anguilla': ['AI'], 'Sao Tome and Principe': ['ST'], 'Djibouti': ['DJ'], 'Mayotte': ['YT'],
'Montserrat': ['MS'], 'Tonga': ['TO'], 'Vanuatu': ['VU'], 'Norfolk Island': ['NF'],
'Solomon Islands': ['SB'], 'Turks and Caicos Islands': ['TC'], 'Northern Mariana Islands': ['MP'],
'Equatorial Guinea': ['GQ'], 'American Samoa': ['AS'], 'Chad': ['TD'], 'Falkland Islands': ['FK'],
'Antarctica': ['AQ'], 'Nauru': ['NR'], 'Niue': ['NU'], 'Saint Pierre and Miquelon': ['PM'],
'Tokelau': ['TK'], 'Tuvalu': ['TV'], 'Wallis and Futuna': ['WF'], 'Korea': ['KR'],
'Antigua & Barbuda': ['AG'], 'Austria-Hungary': ['AT', 'HU'], 'British Virgin Islands': ['VG'],
'Brunei': ['BN'], 'Burma': ['MM'], 'Cape Verde': ['CV'], 'Virgin Islands': ['VI'],
'Vatican City': ['VA'], 'Swaziland': ['SZ'], 'Southern Sudan': ['SS'], 'Palestine': ['PS'],
'Singapore, Malaysia, Hong Kong & Thailand': ['SG', 'MY', 'HK', 'TH'], 'Pitcairn Islands': ['PN'],
'Micronesia, Federated States of': ['FM'], 'Man, Isle of': ['IM'], 'Macau': ['MO'],
'Korea (pre-1945)': ['KR'], 'Hong Kong & Thailand': ['HK', 'TH'], 'Gambia, The': ['GM'],
// 'Africa': ['??'], 'South West Africa': ['??'],
// 'Central America': ['??'], 'North & South America': ['??'],
// 'Asia': ['??'], 'South East Asia': ['??'], Middle East': ['??'], 'Gulf Cooperation Council': ['??'],
// 'South Pacific': ['??'],
// 'Dutch East Indies': ['??'], 'Gaza Strip': ['??'], 'Dahomey': ['??'], 'Indochina': ['??'],
// 'Abkhazia': ['??'], 'Belgian Congo': ['??'], 'Bohemia': ['??'], 'Kosovo': ['??'],
// 'Netherlands Antilles': ['??'], 'Ottoman Empire': ['??'], 'Rhodesia': ['??'],
// 'Russia & CIS': ['??'], 'Southern Rhodesia': ['??'], 'Upper Volta': ['??'], 'West Bank': ['??'],
// 'Zaire': ['??'], 'Zanzibar': ['??'],
}[release.country] || [release.country] : [undefined]).forEach(function(countryCode, countryIndex) {
if (countryCode) formData.set(`events.${countryIndex}.country`, countryCode);
if (released instanceof Date) {
formData.set(`events.${countryIndex}.date.year`, released.getUTCFullYear());
formData.set(`events.${countryIndex}.date.month`, released.getUTCMonth() + 1);
formData.set(`events.${countryIndex}.date.day`, released.getUTCDate());
} else if (released > 0) formData.set(`events.${countryIndex}.date.year`, released);
});
let defaultFormat = 'CD', descriptors = new Set;
if ('formats' in release) {
for (let format of release.formats) {
if (format.text) descriptors.add(format.text);
if (Array.isArray(format.descriptions)) for (let description of format.descriptions)
descriptors.add(description);
}
if (!release.formats.some(format => format.name == 'CD')
&& release.formats.some(format => format.name == 'CDr')) defaultFormat = 'CD-R';
else if (descriptors.has('HDCD')) defaultFormat = 'HDCD';
else if (descriptors.has('CD+G')) defaultFormat = 'CD+G';
}
if (release.labels) release.labels.forEach(function(label, index) {
if (label.name) {
const prefix = 'labels.' + index;
formData.set(prefix + '.name', stripNameSuffix(label.name));
if (!(label.id in lookupIndexes.label)) lookupIndexes.label[label.id] = {
name: label.name.replace(/(?:\s+\b(?:Record(?:ing)?s)\b|,?\s+(?:Ltd|Inc|Co)\.?)+$/i, ''),
prefixes: [prefix],
}; else lookupIndexes.label[label.id].prefixes.push(prefix);
}
if (label.catno) formData.set(`labels.${index}.catalog_number`,
label.catno.toLowerCase() == 'none' ? '[none]' : label.catno);
});
if (release.identifiers) (barcode =>
{ if (barcode) formData.set('barcode', barcode.value.replace(/\D+/g, '')) })
(release.identifiers.find(identifier => identifier.type == 'Barcode'));
seedArtists(release.artists); //seedArtists(release.extraartists);
if (!Array.isArray(cdLengths) || cdLengths.length <= 0) cdLengths = false;
if ([
/^()?()?(\S+)$/,
/^([A-Z]{2,})(?:[\-\ ](\d+))?[\ \-\.](\S+)$/,
/^([A-Z]{2,})?(\d+)?[\ \-\.](\S+)$/,
].some(function(trackParser) {
media = [ ];
let lastMediumId, heading;
(function addTracks(root, titles) {
if (Array.isArray(root)) for (let track of root) switch (track.type_) {
case 'track': {
const parsedTrack = trackParser.exec(track.position.trim());
let [mediumFormat, mediumId, trackPosition] = parsedTrack != null ?
parsedTrack.slice(1) : [undefined, undefined, track.position.trim()];
if ((mediumId = (mediumFormat || '') + (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;
}
media[media.length - 1].tracks.push({
number: trackPosition,
heading: heading,
titles: titles,
name: track.title,
length: track.duration,
artists: track.artists,
extraartists: track.extraartists,
});
break;
}
case 'index':
addTracks(track.sub_tracks, (titles || [ ]).concat(track.title));
break;
case 'heading':
heading = track.title != '-' && track.title || undefined;
break;
}
})(release.tracklist);
for (let medium of media) if (medium.tracks.every((track, ndx, tracks) => track.heading == tracks[0].heading)) {
medium.name = medium.tracks[0].heading;
medium.tracks.forEach(track => { track.heading = undefined });
}
return layoutMatch(media);
}) || !cdLengths || confirm('Tracks seem not mapped correctly to media (' +
media.map(medium => medium.tracks.length).join('+') + ' ≠ ' + cdLengths.join('+') +
'), attach tracks with this layout anyway?'))
media.forEach(function(medium, mediumIndex) {
formData.set(`mediums.${mediumIndex}.format`, medium.format);
if (medium.name) formData.set(`mediums.${mediumIndex}.name`, 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`, fullTitle);
frequencyAnalysis(literals, fullTitle);
}
if (track.length) formData.set(`mediums.${mediumIndex}.track.${trackIndex}.length`, track.length);
if (track.artists) seedArtists(track.artists, `mediums.${mediumIndex}.track.${trackIndex}.`);
//if (track.extraartists) seedArtists(track.extraartists, `mediums.${mediumIndex}.track.${trackIndex}.`);
});
});
const charCodes = Object.keys(literals).map(key => parseInt(key));
if (charCodes.every(charCode => charCode < 0x100)) formData.set('script', 'Latn');
const packagings = {
'book': 'Book', 'box': 'Box', 'cardboard': 'Cardboard/Paper Sleeve',
'paper sleeve': 'Cardboard/Paper Sleeve', 'cassette': 'Cassette Case', 'cassette case': 'Cassette Case',
'clamshell': 'Clamshell Case', 'clamshell case': 'Clamshell Case', 'digibook': 'Digibook',
'digipak': 'Digipak', 'digipack': 'Digipak', 'discbox slider': 'Discbox Slider', 'fatbox': 'Fatbox',
'gatefold': 'Gatefold Cover', 'gatefold cover': 'Gatefold Cover',
'jewel': 'Jewel case', 'jewel case': 'Jewel case', 'keep': 'Keep Case', 'keep case': 'Keep Case',
'longbox': 'Longbox', 'metal tin': 'Metal Tin', 'plastic sleeve': 'Plastic sleeve',
'slidepack': 'Slidepack', 'slim jewel': 'Slim Jewel Case', 'slim jewel case': 'Slim Jewel Case',
'snap': 'Snap Case', 'snap case': 'Snap Case', 'snappack': 'SnapPack',
'super jewel': 'Super Jewel Box', 'super jewel box': 'Super Jewel Box',
};
for (let packaging of new Set(Array.from(descriptors, d => packagings[d.toLowerCase()]).filter(Boolean)))
formData.append('packaging', packaging);
if (descriptors.has('Promo') && formData.get('status') != 'bootleg') formData.set('status', 'promotion');
if ((descriptors = dcFmtFilters.reduce((arr, filter) => arr.filter(filter), Array.from(descriptors))
.filter(desc => !(desc.toLowerCase() in packagings))
.filter(desc => !['Promo'].includes(desc))).length > 0)
formData.set('comment', descriptors.join(', ')/*.toLowerCase()*/); // disambiguation
const annotation = [
release.notes && release.notes.trim(),
release.identifiers && release.identifiers
.filter(identifier => !['Barcode', 'ASIN'].includes(identifier.type))
.map(identifier => identifier.type + ': ' + identifier.value).join('\n'),
release.companies && (function() {
const companies = { };
for (let company of release.companies) if (company.entity_type_name) {
if (!(company.entity_type_name in companies)) companies[company.entity_type_name] = [ ];
companies[company.entity_type_name].push(company);
}
return Object.keys(companies).map(type => type + ' – ' + companies[type].map(company =>
company.catno ? company.name + ' – ' + company.catno : company.name).join(', '));
})().join('\n'),
].filter(Boolean);
if (annotation.length > 0) formData.set('annotation', annotation.join('\n\n'));
let urlRelIndex = -1;
addUrlRef(dcOrigin + '/release/' + release.id, 76);
if (release.identifiers) for (let identifier of release.identifiers) switch (identifier.type) {
case 'ASIN': addUrlRef('https://www.amazon.com/dp/' + identifier.value, 77); break;
}
formData.set('edit_note', ((formData.get('edit_note') || '') +
`\nSeeded from Discogs release id ${release.id}`).trimLeft());
return idsLookupLimit > 0 ? Promise.all(Object.keys(lookupIndexes).map(entity =>
Promise.all(Object.keys(lookupIndexes[entity]).map(discogsId => mbApiRequest(entity, {
query: '"' + lookupIndexes[entity][discogsId].name + '"',
limit: idsLookupLimit,
}).then(results => results[entity + 's'].map(result => result.id)).then(mbids => (function findDiscogsRelative(index = 0) {
return index < mbids.length ? mbApiRequest(entity + '/' + mbids[index], { inc: 'url-rels' })
.then(release => release.relations.some(relation => relation.type == 'discogs'
&& discogsIdExtractor(relation.url.resource, entity) == parseInt(discogsId)) ?
release.id : Promise.reject('No Discogs relative'))
.catch(reason => findDiscogsRelative(index + 1)) : Promise.reject('No Discogs relatives');
})()).catch(reason => null))))).then(function(lookupResults) {
Object.keys(lookupIndexes).forEach(function(entity, ndx1) {
Object.keys(lookupIndexes[entity]).forEach(function(discogsId, ndx2) {
if (lookupResults[ndx1][ndx2] != null) for (let prefix of lookupIndexes[entity][discogsId].prefixes)
formData.set(prefix + '.mbid', lookupResults[ndx1][ndx2]);
});
});
return formData;
}) : formData;
}) : Promise.reject('Invalid Discogs ID');
}
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', heb: 'Hebr', ara: 'Arab',
gre: 'Grek', ell: 'Grek', rus: 'Cyrl', 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());
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(' ')) };
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);
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('Not logged in or unable to reliably bind volumes');
if (!confirm(`${mbTOCs.length} TOCs are going to be attached to release id ${mbId}
${mediums.length > 1 ? '\nMedia 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 deduction for uncalibrated read offset`)) 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) {
return addClickableIcon('<svg version="1.1" height="0.9em" viewBox="0 0 53.61 53.61"><path style="fill:none; stroke:#14A085; stroke-width:4; stroke-linecap:round; stroke-miterlimit:10;" d="M31.406,18.422L14.037,35.792c-1.82,1.82-1.82,4.797,0,6.617l0,0c1.82,1.82,4.797,1.82,6.617,0l25.64-25.64c3.412-3.412,2.998-8.581-0.414-11.993l0,0c-3.412-3.412-8.581-3.825-11.993-0.414L6.593,31.656c-4.549,4.549-4.549,11.993,0,16.542l0,0c4.549,4.549,11.993,4.549,16.542,0l27.295-27.295"/></svg>', function(evt) {
if (evt.currentTarget.disabled) return; else evt.stopPropagation();
callback(evt.altKey, evt.ctrlKey);
}, !style ? function(evt) {
let mbId = evt.dataTransfer.getData('text/plain');
if (mbId && (mbId = mbId.split(/(?:\r?\n)+/)).length > 0 && (mbId = mbIdExtractor(mbId[0], 'release')))
attachToMB(mbId, evt.altKey, evt.ctrlKey);
return false;
} : undefined, 'attach-toc', style, tooltip, tooltipster);
}
function seedToMB(target, torrent, discogsId, releaseGroupId) {
if (!(target instanceof HTMLElement) || !torrent) throw 'Invalid argument';
getMbTOCs().then(function(mbTOCs) {
const formData = new URLSearchParams;
if (rxMBID.test(releaseGroupId)) formData.set('release_group', releaseGroupId);
seedFromTorrent(formData, torrent);
return seedFromTOCs(formData, mbTOCs).then(formData => discogsId > 0 || discogsId < 0 ?
seedFromDiscogs(formData, discogsId, mbTOCs.map(mbTOC => mbTOC[1])) : formData);
}).then(seedNewRelease).catch(alert).then(target.epilogue);
}
function seedToMBIcon(callback, style, tooltip, tooltipster) {
const staticIcon = '<svg height="0.9em" fill="#0a0" viewBox="0 0 33.43 33.43"><path d="M0 21.01l0 -8.59c0,-0.37 0.3,-0.67 0.67,-0.67l11.08 0 0 -11.08c0,-0.37 0.3,-0.67 0.67,-0.67l8.59 0c0.37,0 0.67,0.3 0.67,0.67l0 11.08 11.08 0c0.37,0 0.67,0.3 0.67,0.67l0 8.59c0,0.37 -0.3,0.67 -0.67,0.67l-11.08 0 0 11.08c0,0.37 -0.3,0.67 -0.67,0.67l-8.59 0c-0.37,0 -0.67,-0.3 -0.67,-0.67l0 -11.08 -11.08 0c-0.37,0 -0.67,-0.3 -0.67,-0.67z"/></svg>';
const aniSpinner = '<svg fill="orange" style="scale: 2.5;" height="0.9em" viewBox="0 0 100 100">' +
((phases = 12) => Array.from(Array(phases)).map((_, ndx) => `<g transform="rotate(${(phases - ndx) * 360 / phases} 50 50)"><rect x="47.5" y="27" rx="2.5" ry="4.16" width="5" height="16"><animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="${-1000 * ndx / phases}ms" repeatCount="indefinite"></animate></rect></g>`))(12).join('') + '</svg>';
const aniGear = '<svg fill="orange" style="scale: 1.1;" height="0.9em" viewBox="-50 -50 100 100"><g transform="translate(50 50)"><animateTransform attributeName="transform" type="rotate" values="0;45" keyTimes="0;1" dur="333ms" repeatCount="indefinite"></animateTransform><path d="M34.856850115866756 -9 L48.856850115866756 -9 L48.856850115866756 9 L34.856850115866756 9 A36 36 0 0 1 31.0114761184114 18.28355405705355 L31.0114761184114 18.28355405705355 L40.91097105502307 28.183048993665217 L28.183048993665217 40.91097105502307 L18.28355405705355 31.0114761184114 A36 36 0 0 1 9 34.856850115866756 L9 34.856850115866756 L9 48.856850115866756 L-8.999999999999996 48.856850115866756 L-8.999999999999996 34.856850115866756 A36 36 0 0 1 -18.28355405705354 31.011476118411405 L-18.28355405705354 31.011476118411405 L-28.183048993665203 40.91097105502307 L-40.91097105502307 28.183048993665206 L-31.0114761184114 18.283554057053543 A36 36 0 0 1 -34.85685011586675 9.00000000000001 L-34.85685011586675 9.00000000000001 L-48.85685011586675 9.000000000000012 L-48.856850115866756 -9 L-34.856850115866756 -9.000000000000002 A36 36 0 0 1 -31.01147611841141 -18.283554057053536 L-31.01147611841141 -18.283554057053536 L-40.910971055023076 -28.1830489936652 L-28.183048993665217 -40.91097105502307 L-18.28355405705355 -31.0114761184114 A36 36 0 0 1 -9.000000000000012 -34.85685011586675 L-9.000000000000012 -34.85685011586675 L-9.000000000000016 -48.85685011586675 L8.999999999999995 -48.856850115866756 L8.999999999999998 -34.856850115866756 A36 36 0 0 1 18.283554057053536 -31.01147611841141 L18.283554057053536 -31.01147611841141 L28.1830489936652 -40.910971055023076 L40.91097105502307 -28.183048993665217 L31.0114761184114 -18.28355405705355 A36 36 0 0 1 34.85685011586675 -9.000000000000014 M0 -20A20 20 0 1 0 0 20 A20 20 0 1 0 0 -20"></path></g></svg>';
const span = addClickableIcon(staticIcon, function(evt) {
if (evt.currentTarget.disabled) return; else evt.stopPropagation();
let discogsId = evt.ctrlKey ? prompt(`Enter Discogs release ID or URL:
(note the data preparation process may take some time due to MB API rate limits, especially for compilations)
`) : undefined;
if (discogsId === null) return;
if (discogsId != undefined && !((discogsId = discogsIdExtractor(discogsId, 'release')) > 0))
return alert('Invalid input');
evt.currentTarget.prologue(discogsId > 0 && !evt.shiftKey);
callback(evt.currentTarget, evt.shiftKey ? -discogsId : discogsId);
}, function(evt) {
let data = evt.dataTransfer.getData('text/plain'), id;
if (data && (data = data.split(/(?:\r?\n)+/)).length > 0) {
if ((id = discogsIdExtractor(data[0], 'release')) > 0) {
evt.currentTarget.prologue(!evt.shiftKey);
callback(evt.currentTarget, evt.shiftKey ? -id : id);
} else if (id = mbIdExtractor(data[0], 'release-group')) callback(evt.currentTarget, id);
}
return false;
}, 'seed-mb-release', style, tooltip, tooltipster);
span.prologue = function(waitingStatus = true) {
if (this.disabled) return false; else this.disabled = true;
if (waitingStatus) {
this.classList.add('in-progress');
this.innerHTML = aniSpinner;
}
return true;
}.bind(span);
span.epilogue = function() {
if (this.classList.contains('in-progress')) {
this.innerHTML = staticIcon;
this.classList.remove('in-progress');
}
this.disabled = false;
}.bind(span);
return span;
}
if (target.disabled) return; else target.disabled = true;
[target.textContent, target.style.color] = ['Looking up...', null];
const getMbTOCs = () => lookupByToc(torrentId, tocEntries => Promise.resolve(tocEntriesToMbTOC(tocEntries)));
const mbID = /([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})/i.source;
const rxMBID = new RegExp(`^${mbID}$`, 'i');
const isCD = medium => /\b(?:(?:H[DQ])?CD|CDr)\b/.test(medium.format);
const sameMedia = release =>
release.media.every(medium => !medium.format) ? release.media : release.media.filter(isCD);
const dcFmtFilters = [
fmt => fmt && !['CD', 'Album', 'Single', 'EP', 'LP', 'Compilation', 'Stereo'].includes(fmt),
fmt => !fmt || !['CDV', 'CD-ROM', 'SVCD', 'VCD'].includes(fmt),
description => description && !['Mini-Album', 'Digipak', 'Digipack', 'Sampler'/*, 'Maxi-Single'*/].includes(description),
];
const scriptSignature = 'Edition lookup by CD TOC browser script (https://greasyfork.org/scripts/459083)';
const formattedJoinPhrase = joinPhrase => joinPhrase && [
[/^\s*(?:Feat(?:uring)?|Ft)\.?\s*$/i, ' feat. '], [/^\s*([\,\;])\s*$/, '$1 '],
[/^\s*([\&\+\/\x\×]|vs\.|w\/|\w+)\s*$/i, (m, join) => ' ' + join.toLowerCase() + ' '],
[/^\s*(?:,\s*(?:and|&|with))\s*$/i, ', $1 '],
].reduce((phrase, subst) => phrase.replace(...subst), joinPhrase);
const mbAttachMode = Number(GM_getValue('mb_attach_toc', 2));
const mbSubmitLog = GM_getValue('mb_submit_log', false);
const mbSeedNew = Number(GM_getValue('mb_seed_release', true));
lookupByToc(torrentId, (tocEntries, discNdx, totalDiscs) =>
mbLookupByDiscID(tocEntriesToMbTOC(tocEntries), !evt.ctrlKey)).then(function(results) {
if (mbSeedNew) target.after(seedToMBIcon(function(target, id) {
queryAjaxAPICached('torrent', { id: torrentId })
.then(torrent => { seedToMB(target, torrent, id, id) }, alert);
}, undefined, `Seed new MusicBrainz release from this CD TOC
Use Ctrl or drop Discogs release link to import Discogs metadata (+ Shift skips MBID lookup - faster, use when adding to exising release group)
Drop exising MusicBrainz release group link to seed to this group
MusicBrainz account required`, true));
if (mbAttachMode > 0) target.after(attachToMBIcon(function(attended, skipPoll) {
attachToMB(undefined, attended, skipPoll);
}, undefined, 'Attach this CD TOC by hand to release not shown in lookup results\nMusicBrainz account required', true));
if (results.length <= 0 || results[0] == null) {
if (!evt.ctrlKey) target.dataset.haveResponse = true;
return Promise.reject('No matches');
} else target.dataset.haveResponse = true;
const isSameRemaster = release => !release.media || sameMedia(release).length == results.length;
let releases = results[0].releases.filter(isSameRemaster);
const score = releases.length > 0 ? results.every(result => result != null) ?
results.every(result => result.attached) ? 0 : results.some(result => result.attached) ? 1 : 2
: results.some(result => result != null && result.attached) ? 3 : 4 : 5;
if (releases.length <= 0) releases = results[0].releases;
target.dataset.ids = JSON.stringify(releases.map(release => release.id));
[target.dataset.discId, target.dataset.toc] = [results[0].mbDiscID, JSON.stringify(results[0].mbTOC)];
(function(type, color) {
type = `${releases.length} ${type} match`;
target.textContent = releases.length != 1 ? type + 'es' : type;
target.style.color = color;
})(...[
['exact', '#0a0'], ['hybrid', '#3a0'], ['fuzzy', '#6a0'],
['partial', '#9a0'], ['partial', '#ca0'], ['unlikely', '#f80'],
][score]);
if (GM_getValue('auto_open_tab', true) && score < 2) GM_openInTab(mbOrigin + '/cdtoc/' +
(evt.shiftKey ? 'attach?toc=' + results[0].mbTOC.join(' ') : results[0].mbDiscID), true);
if (score < 5) return queryAjaxAPICached('torrent', { id: torrentId }).then(function(torrent) {
function appendDisambiguation(elem, disambiguation) {
if (!(elem instanceof HTMLElement) || !disambiguation) return;
const span = document.createElement('SPAN');
span.className = 'disambiguation';
span.style.opacity = 0.6;
span.textContent = '(' + disambiguation + ')';
elem.append(' ', span);
}
const isCompleteInfo = torrent.torrent.remasterYear > 0
&& Boolean(torrent.torrent.remasterRecordLabel)
&& Boolean(torrent.torrent.remasterCatalogueNumber);
const is = what => !torrent.torrent.remasterYear && {
unknown: torrent.torrent.remastered,
unconfirmed: !torrent.torrent.remastered,
}[what];
const labelInfoMapper = release => Array.isArray(release['label-info']) ?
release['label-info'].map(labelInfo => ({
label: labelInfo.label && labelInfo.label.name,
catNo: labelInfo['catalog-number'],
})).filter(labelInfo => labelInfo.label || labelInfo.catNo) : [ ];
// add inpage search results
const [thead, table, tbody] = ['DIV', 'TABLE', 'TBODY'].map(Document.prototype.createElement.bind(document));
thead.style = 'margin-bottom: 5pt;';
thead.innerHTML = `<b>Applicable MusicBrainz matches</b> (${[
'exact',
`${results.filter(result => result != null && result.attached).length} exact out of ${results.length} matches`,
'fuzzy',
`${results.filter(result => result != null && result.attached).length} exact / ${results.filter(result => result != null).length} matches out of ${results.length}`,
`${results.filter(result => result != null).length} matches out of ${results.length}`,
][score]})`;
table.style = 'margin: 0; max-height: 10rem; overflow-y: auto; empty-cells: hide;';
table.className = 'mb-lookup-results mb-lookup-' + torrent.torrent.id;
tbody.dataset.torrentId = torrent.torrent.id;
tbody.dataset.edition = target.parentNode.dataset.edition;
releases.forEach(function(release, index) {
const [tr, artist, title, _release, editionInfo, barcode, groupSize, releasesWithId] =
['TR', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD'].map(Document.prototype.createElement.bind(document));
tr.className = 'musicbrainz-release';
tr.style = 'word-wrap: break-word; transition: color 200ms ease-in-out;';
if (release.quality == 'low') tr.style.opacity = 0.75;
tr.dataset.url = 'https://musicbrainz.org/release/' + release.id;
[_release, barcode].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[groupSize, releasesWithId].forEach(elem => { elem.style.textAlign = 'right' });
if ('artist-credit' in release) release['artist-credit'].forEach(function(artistCredit, index, artists) {
if ('artist' in artistCredit && artistCredit.artist.id && ![
'89ad4ac3-39f7-470e-963a-56509c546377',
].includes(artistCredit.artist.id)) {
const a = document.createElement('A');
if (artistCredit.artist) a.href = 'https://musicbrainz.org/artist/' + artistCredit.artist.id;
[a.target, a.style, a.textContent, a.className] =
['_blank', noLinkDecoration, artistCredit.name, 'musicbrainz-artist'];
if (artistCredit.artist) a.title = artistCredit.artist.disambiguation || artistCredit.artist.id;
artist.append(a);
} else artist.append(artistCredit.name);
if (index < artists.length - 1) artist.append(artistCredit.joinphrase || ' & ');
});
title.innerHTML = linkHTML(tr.dataset.url, release.title, 'musicbrainz-release');
switch (release.quality) {
case 'low': title.insertAdjacentHTML('afterbegin', svgBulletHTML('#ff6723')); break;
case 'high': title.insertAdjacentHTML('afterbegin', svgBulletHTML('#00d26a')); break;
}
appendDisambiguation(title, release.disambiguation);
// attach CD TOC
if (mbAttachMode > 0 && (score > 0 || results.some(medium => !medium.releases.some(_release =>
_release.id == release.id)))) title.prepend(attachToMBIcon(function(attended, skipPoll) {
attachToMB(release.id, attended, skipPoll);
}, 'float: right; margin-left: 4pt;', `Attach CD TOC to release (verify CD rip and MB release are identical edition)
Submission mode: ${mbAttachMode > 1 ? 'unattended (Alt+click enforces attended mode, Ctrl+click disables poll)' : 'attended'}
MusicBrainz account required`));
// Seed new edition
if (mbSeedNew) title.prepend(seedToMBIcon(function(target, discogsId) {
seedToMB(target, torrent, discogsId, release['release-group'].id);
}, 'float: right; margin-left: 4pt;', `Seed new MusicBrainz edition from this CD TOC in same release group
Use Ctrl or drop Discogs release link to import Discogs metadata (+ Shift skips MBIDs lookup - faster)
MusicBrainz account required`));
_release.innerHTML = 'release-events' in release && release['release-events'].map(releaseEvent =>
releaseEvent.area && Array.isArray(releaseEvent.area['iso-3166-1-codes'])
&& releaseEvent.area['iso-3166-1-codes'].map(countryCode =>
releaseEventToHtml(countryCode, releaseEvent.date)).filter(Boolean).join('<br>')
|| releaseEventToHtml(undefined, releaseEvent.date)).filter(Boolean).join('<br>')
|| releaseToHtml(release);
if ('label-info' in release) editionInfo.innerHTML = release['label-info'].map(labelInfo => [
labelInfo.label && labelInfo.label.name && `<span class="label">${labelInfo.label.name}</span>`,
labelInfo['catalog-number'] && `<span class="catno" style="white-space: nowrap;">${labelInfo['catalog-number']}</span>`,
].filter(Boolean).join(' ')).filter(Boolean).join('<br>');
if (editionInfo.childElementCount <= 0) mbFindEditionInfoInAnnotation(editionInfo, release.id);
if (release.barcode) barcode.textContent = release.barcode;
if (release['release-group']) {
tr.dataset.groupUrl = 'https://musicbrainz.org/release-group/' + release['release-group'].id;
mbApiRequest('release-group/' + release['release-group'].id, {
inc: 'releases media discids',
}).then(releaseGroup => releaseGroup.releases.filter(isSameRemaster)).then(function(releases) {
const a = document.createElement('A');
a.href = 'https://musicbrainz.org/release-group/' + release['release-group'].id;
[a.target, a.style, a.textContent] = ['_blank', noLinkDecoration, releases.length];
if (releases.length == 1) a.style.color = '#0a0';
groupSize.append(a);
groupSize.title = 'Same media count in release group';
const counts = ['some', 'every'].map(fn => releases.filter(release => release.media
&& (release = sameMedia(release)).length > 0
&& release[fn](medium => medium.discs && medium.discs.length > 0)).length);
releasesWithId.textContent = counts[0] > counts[1] ? counts[0] + '/' + counts[1] : counts[1];
releasesWithId.title = 'Same media count with known TOC in release group';
}, function(reason) {
if (releasesWithId.parentNode != null) releasesWithId.remove();
[groupSize.colSpan, groupSize.innerHTML, groupSize.title] = [2, svgFail('1em'), reason];
});
}
try {
if (isCompleteInfo || !('edition' in target.parentNode.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey) throw 'Not applicable';
const releaseYear = getReleaseYear(release.date), editionInfo = labelInfoMapper(release);
if (!(releaseYear > 0) || editionInfo.length <= 0 && !release.barcode
&& torrent.torrent.remasterYear > 0) throw 'Nothinng to update';
tr.dataset.releaseYear = releaseYear;
if (editionInfo.length > 0) tr.dataset.editionInfo = JSON.stringify(editionInfo);
if (release.barcode) tr.dataset.barcodes = JSON.stringify([ release.barcode ]);
if (release.disambiguation) tr.dataset.editionTitle = release.disambiguation;
if (!torrent.torrent.description.includes(release.id))
tr.dataset.description = torrent.torrent.description.trim();
applyOnClick(tr);
} catch(e) { openOnClick(tr) }
(tr.title ? title.querySelector('a.musicbrainz-release') : tr).title = [
release.quality && release.quality != 'normal' && release.quality + ' quality',
release.media && release.media.map(medium => medium.format).join(' + '),
[release.status != 'Official' && release.status, release.packaging].filter(Boolean).join(' / '),
release.id,
].filter(Boolean).join('\n');
tr.append(artist, title, _release, editionInfo, barcode, groupSize, releasesWithId);
['artist', 'title', 'release-event', 'edition-info', 'barcode', 'releases-count', 'discids-count']
.forEach((className, index) => tr.cells[index].className = className);
tbody.append(tr);
if (release.relations) for (let relation of release.relations) {
if (relation.type != 'discogs' || !relation.url) continue;
let discogsId = /\/releases?\/(\d+)\b/i.exec(relation.url.resource);
if (discogsId != null) discogsId = parseInt(discogsId[1]); else continue;
if (title.querySelector('span.have-discogs-relatives') == null) {
const span = document.createElement('SPAN');
span.innerHTML = GM_getResourceText('dc_icon');
span.firstElementChild.setAttribute('height', 6);
span.firstElementChild.removeAttribute('width');
span.firstElementChild.style.verticalAlign = 'top';
svgSetTitle(span.firstElementChild, 'Has defined Discogs relative(s)');
span.className = 'have-discogs-relatives';
title.append(' ', span);
}
dcApiRequest('releases/' + discogsId).then(function(release) {
const [trDc, icon, artist, title, _release, editionInfo, barcode, groupSize] =
['TR', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD'].map(Document.prototype.createElement.bind(document));
trDc.className = 'discogs-release';
trDc.style = 'background-color: #8882; word-wrap: break-word; transition: color 200ms ease-in-out;';
trDc.dataset.url = dcOrigin + '/release/' + release.id;
[_release, barcode].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[groupSize, icon].forEach(elem => { elem.style.textAlign = 'right' });
if (release.artists) release.artists.forEach(function(artistCredit, index, artists) {
if (artistCredit.id > 0 && ![194].includes(artistCredit.id)) {
const a = document.createElement('A');
if (artistCredit.id) a.href = dcOrigin + '/artist/' + artistCredit.id;
[a.target, a.style, a.className, a.title] =
['_blank', noLinkDecoration, 'discogs-artist', artistCredit.role || artistCredit.id];
a.textContent = artistCredit.anv || stripNameSuffix(artistCredit.name);
artist.append(a);
} else artist.append(artistCredit.anv || stripNameSuffix(artistCredit.name));
if (index < artists.length - 1) artist.append(formattedJoinPhrase(artistCredit.join) || ' & ');
});
title.innerHTML = linkHTML(trDc.dataset.url, release.title, 'discogs-release');
const fmtCDFilter = fmt => ['CD', 'CDr', 'All Media'].includes(fmt);
let descriptors = [ ];
if ('formats' in release) for (let format of release.formats) if (fmtCDFilter(format.name)
&& dcFmtFilters[1](format.text)
&& (!Array.isArray(format.descriptions) || format.descriptions.every(dcFmtFilters[1]))) {
if (dcFmtFilters[0](format.text)) descriptors.push(format.text);
if (Array.isArray(format.descriptions))
Array.prototype.push.apply(descriptors, format.descriptions.filter(dcFmtFilters[0]));
}
descriptors = descriptors.filter((d1, n, a) => a.findIndex(d2 => d2.toLowerCase() == d1.toLowerCase()) == n);
if (descriptors.length > 0) appendDisambiguation(title, descriptors.join(', '));
_release.innerHTML = [
release.country && `<span class="country">${release.country}</span>`, // `<span class="country"><img src="http://s3.cuetools.net/flags/${release.country.toLowerCase()}.png" height="9" title="${release.country}" onerror="this.replaceWith(this.title)" /></span>`,
release.released && `<span class="date">${release.released}</span>`,
].filter(Boolean).join(' ');
if (Array.isArray(release.labels)) editionInfo.innerHTML = release.labels.map(label => [
label.name && `<span class="label">${stripNameSuffix(label.name)}</span>`,
label.catno && `<span class="catno" style="white-space: nowrap;">${label.catno}</span>`,
].filter(Boolean).join(' ')).filter(Boolean).join('<br>');
let barCode = release.identifiers && release.identifiers.find(id => id.type == 'Barcode');
if (barCode) barCode = barCode.value.replace(/\D+/g, '');
if (barCode) barcode.textContent = barCode;
icon.innerHTML = GM_getResourceText('dc_icon');
icon.firstElementChild.style = '';
icon.firstElementChild.removeAttribute('width');
icon.firstElementChild.setAttribute('height', '1em');
svgSetTitle(icon.firstElementChild, release.id);
if (release.master_id) {
const masterUrl = new URL('/master/' + release.master_id, dcOrigin);
for (let format of ['CD', 'CDr']) masterUrl.searchParams.append('format', format);
masterUrl.hash = 'versions';
trDc.dataset.groupUrl = masterUrl;
const getGroupSize1 = () => dcApiRequest(`masters/${release.master_id}/versions`)
.then(({filters}) => (filters = filters && filters.available && filters.available.format) ?
['CD', 'CDr'].reduce((s, f) => s + (filters[f] || 0), 0) : Promise.reject('Filter totals missing'));
const getGroupSize2 = (page = 1) => dcApiRequest(`masters/${release.master_id}/versions`, {
page: page,
per_page: 1000,
}).then(function(versions) {
const releases = versions.versions.filter(version => !Array.isArray(version.major_formats)
|| version.major_formats.some(fmtCDFilter)).length;
if (!(versions.pagination.pages > versions.pagination.page)) return releases;
return getGroupSize2(page + 1).then(releasesNxt => releases + releasesNxt);
});
getGroupSize1().catch(reason => getGroupSize2()).then(function(_groupSize) {
const a = document.createElement('A');
a.href = masterUrl; a.target = '_blank';
a.style = noLinkDecoration;
a.textContent = _groupSize;
if (_groupSize == 1) a.style.color = '#0a0';
groupSize.append(a);
groupSize.title = 'Total of same media versions for master release';
}, function(reason) {
groupSize.style.paddingTop = '5pt';
groupSize.innerHTML = svgFail('1em');
groupSize.title = reason;
});
} else {
groupSize.textContent = '-';
groupSize.style.color = '#0a0';
groupSize.title = 'Without master release';
}
try {
if (isCompleteInfo || !('edition' in target.parentNode.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey) throw 'Not applicable';
const releaseYear = getReleaseYear(release.released);
if (!(releaseYear > 0)) throw 'Year unknown';
const editionInfo = Array.isArray(release.labels) ? release.labels.map(label => ({
label: stripNameSuffix(label.name),
catNo: label.catno,
})).filter(label => label.label || label.catNo) : [ ];
if (editionInfo.length <= 0 && !barCode && torrent.torrent.remasterYear > 0)
throw 'Nothing to update';
trDc.dataset.releaseYear = releaseYear;
if (editionInfo.length > 0) trDc.dataset.editionInfo = JSON.stringify(editionInfo);
if (barCode) trDc.dataset.barcodes = JSON.stringify([ barCode ]);
if ((descriptors = descriptors.filter(dcFmtFilters[2])).length > 0)
trDc.dataset.editionTitle = descriptors.join(' / ');
if (!torrent.torrent.description.includes(trDc.dataset.url))
trDc.dataset.description = torrent.torrent.description.trim();
applyOnClick(trDc);
} catch(e) { openOnClick(trDc) }
(trDc.title ? title.querySelector('a.discogs-release') : trDc).title = release.formats.map(function(format) {
const tags = [format.text].concat(format.descriptions || [ ]).filter(Boolean);
if (format.name == 'All Media') return tags.length > 0 && tags.join(', ');
let description = format.qty + '×' + format.name;
if (tags.length > 0) description += ' (' + tags.join(', ') + ')';
return description;
}).concat((release.series || [ ]).map(series => 'Series: ' + series))
.concat((release.identifiers || [ ]).filter(identifier => identifier.type != 'Barcode')
.map(identifier => identifier.type + ': ' + identifier.value))
.concat([
[release.data_quality, release.status].filter(Boolean).join(' / '),
release.id,
]).filter(Boolean).join('\n');
trDc.append(artist, title, _release, editionInfo, barcode, groupSize, icon);
['artist', 'title', 'release-event', 'edition-info', 'barcode', 'releases-count', 'discogs-icon']
.forEach((className, index) => trDc.cells[index].className = className);
tr.after(trDc); //tbody.append(trDc);
}, reason => { svgSetTitle(title.querySelector('span.have-discogs-relatives').firstElementChild, reason) });
}
});
table.append(tbody);
addLookupResults(torrentId, thead, table);
if (isCompleteInfo || !('edition' in target.parentNode.dataset) || score > (is('unknown') ? 0 : 3)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey
|| torrent.torrent.remasterYear > 0 && !(releases = releases.filter(release =>
!release.date || getReleaseYear(release.date) == torrent.torrent.remasterYear))
.some(release => release['label-info'] && release['label-info'].length > 0 || release.barcode)
|| releases.length > (is('unknown') ? 1 : 3)) return;
const releaseYear = releases.reduce((year, release) =>
year > 0 ? year : getReleaseYear(release.date), undefined);
if (!(releaseYear > 0) || releases.some(release1 => releases.some(release2 =>
getReleaseYear(release2.date) != getReleaseYear(release1.date)))
|| !releases.every((release, ndx, arr) =>
release['release-group'].id == arr[0]['release-group'].id)) return;
const a = document.createElement('A');
a.className = 'update-edition';
a.href = '#';
a.textContent = '(set)';
a.style.fontWeight = score <= 0 && releases.length < 2 ? 'bold' : 300;
a.dataset.releaseYear = releaseYear;
const editionInfo = Array.prototype.concat.apply([ ], releases.map(labelInfoMapper));
if (editionInfo.length > 0) a.dataset.editionInfo = JSON.stringify(editionInfo);
const barcodes = releases.map(release => release.barcode).filter(Boolean);
if (barcodes.length > 0) a.dataset.barcodes = JSON.stringify(barcodes);
if (releases.length < 2 && releases[0].disambiguation)
a.dataset.editionTitle = releases[0].disambiguation;
if (releases.length < 2 && !torrent.torrent.description.includes(releases[0].id)) {
a.dataset.url = 'https://musicbrainz.org/release/' + releases[0].id;
a.dataset.description = torrent.torrent.description.trim();
}
setTooltip(a, 'Update edition info from matched release(s)\n\n' + releases.map(release =>
release['label-info'].map(labelInfo => [getReleaseYear(release.date), [
labelInfo.label && labelInfo.label.name,
labelInfo['catalog-number'] || release.barcode,
].filter(Boolean).join(' / ')].filter(Boolean).join(' - ')).filter(Boolean).join('\n')).join('\n'));
a.onclick = updateEdition;
if (is('unknown') || releases.length > 1) a.dataset.confirm = true;
target.after(a);
}, alert);
}).catch(function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
}
}, 'Lookup edition on MusicBrainz by Disc ID/TOC (Ctrl enforces strict TOC matching)\nUse Alt to lookup by CDDB ID');
addLookup('GnuDb', function(evt) {
const target = evt.currentTarget;
console.assert(target instanceof HTMLElement);
const entryUrl = entry => `https://gnudb.org/cd/${entry[1].slice(0, 2)}${entry[2]}`;
if (Boolean(target.dataset.haveResponse)) {
if (!('entries' in target.dataset)) return;
for (let entry of JSON.parse(target.dataset.entries).reverse()) GM_openInTab(entryUrl(entry), false);
return;
} else if (target.disabled) return; else target.disabled = true;
target.textContent = 'Looking up...';
target.style.color = null;
lookupByToc(parseInt(target.parentNode.dataset.torrentId), function(tocEntries) {
console.info('Local CDDB ID:', getCDDBiD(tocEntries));
console.info('Local AR ID:', getARiD(tocEntries));
const reqUrl = new URL('https://gnudb.gnudb.org/~cddb/cddb.cgi');
let tocDef = [tocEntries.length].concat(tocEntries.map(tocEntry => preGap + tocEntry.startSector));
const tt = preGap + tocEntries[tocEntries.length - 1].endSector + 1 - tocEntries[0].startSector;
tocDef = tocDef.concat(Math.floor(tt / msf)).join(' ');
reqUrl.searchParams.set('cmd', `discid ${tocDef}`);
reqUrl.searchParams.set('hello', `name ${document.domain} userscript.js 1.0`);
reqUrl.searchParams.set('proto', 6);
return globalXHR(reqUrl, { responseType: 'text' }).then(function({responseText}) {
console.log('GnuDb CDDB discid:', responseText);
const response = /^(\d+) Disc ID is ([\da-f]{8})$/i.exec(responseText.trim());
if (response == null) return Promise.reject(`Unexpected response format (${responseText})`);
console.assert((response[1] = parseInt(response[1])) == 200);
reqUrl.searchParams.set('cmd', `cddb query ${response[2]} ${tocDef}`);
return globalXHR(reqUrl, { responseType: 'text', context: response });
}).then(function({responseText}) {
console.log('GnuDb CDDB query:', responseText);
let entries = /^(\d+)\s+(.+)/.exec((responseText = responseText.trim().split(/\r?\n/))[0]);
if (entries == null) return Promise.reject('Unexpected response format');
const statusCode = parseInt(entries[1]);
if (statusCode < 200 || statusCode >= 400) return Promise.reject(`Server response error (${statusCode})`);
if (statusCode == 202) return Promise.reject('No matches');
entries = (statusCode >= 210 ? responseText.slice(1) : [entries[2]])
.map(RegExp.prototype.exec.bind(/^(\w+)\s+([\da-f]{8})\s+(.*)$/i)).filter(Boolean);
return entries.length <= 0 ? Promise.reject('No matches')
: { status: statusCode, discId: arguments[0].context[2], entries: entries };
});
}).then(function(results) {
if (results.length <= 0 || results[0] == null) return Promise.reject('No matches');
let caption = `${results[0].entries.length} ${['exact', 'fuzzy'][results[0].status % 10]} match`;
if (results[0].entries.length > 1) caption += 'es';
target.textContent = caption;
target.style.color = '#0a0';
if (results[0].entries.length <= 5) for (let entry of Array.from(results[0].entries).reverse())
GM_openInTab(entryUrl(entry), true);
target.dataset.entries = JSON.stringify(results[0].entries);
target.dataset.haveResponse = true;
}).catch(function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
}, 'Lookup edition on GnuDb (CDDB)');
addLookup('CTDB', function(evt) {
const target = evt.currentTarget;
console.assert(target instanceof HTMLElement);
if (target.disabled) return; else target.disabled = true;
const torrentId = parseInt(target.parentNode.dataset.torrentId);
if (!(torrentId > 0)) throw 'Assertion failed: invalid torrentId';
lookupByToc(torrentId, function(tocEntries) {
if (tocEntries.length > 100) throw 'TOC size exceeds limit';
tocEntries = tocEntries.map(tocEntry => tocEntry.endSector + 1 - tocEntries[0].startSector);
return Promise.resolve(new DiscID().addValues(tocEntries, 8, 100).toDigest());
}).then(function(tocIds) {
if (!Boolean(target.parentNode.dataset.haveQuery) && !GM_getValue('auto_open_tab', true)) return;
for (let tocId of Array.from(tocIds).reverse()) if (tocId != null)
GM_openInTab('https://db.cue.tools/?tocid=' + tocId, !Boolean(target.parentNode.dataset.haveQuery));
}, function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.disabled = false });
if (!target.parentNode.dataset.edition || Boolean(target.parentNode.dataset.haveQuery)) return;
const ctdbLookup = params => lookupByToc(torrentId, function(tocEntries, volumeNdx) {
const url = new URL('https://db.cue.tools/lookup2.php');
url.searchParams.set('version', 3);
url.searchParams.set('ctdb', 1);
if (params) for (let param in params) url.searchParams.set(param, params[param]);
url.searchParams.set('toc', tocEntries.map(tocEntry => tocEntry.startSector)
.concat(tocEntries.pop().endSector + 1).join(':'));
const saefInt = (base, property) =>
isNaN(property = parseInt(base.getAttribute(property))) ? undefined : property;
return globalXHR(url).then(({responseXML}) => ({
metadata: Array.from(responseXML.getElementsByTagName('metadata'), metadata => ({
source: metadata.getAttribute('source') || undefined,
id: metadata.getAttribute('id') || undefined,
artist: metadata.getAttribute('artist') || undefined,
album: metadata.getAttribute('album') || undefined,
year: saefInt(metadata, 'year'),
discNumber: saefInt(metadata, 'discnumber'),
discCount: saefInt(metadata, 'disccount'),
release: Array.from(metadata.getElementsByTagName('release'), release => ({
date: release.getAttribute('date') || undefined,
country: release.getAttribute('country') || undefined,
})),
labelInfo: Array.from(metadata.getElementsByTagName('label'), label => ({
name: label.getAttribute('name') || undefined,
catno: label.getAttribute('catno') || undefined,
})),
barcode: metadata.getAttribute('barcode') || undefined,
relevance: saefInt(metadata, 'relevance'),
})),
entries: Array.from(responseXML.getElementsByTagName('entry'), entry => ({
confidence: saefInt(entry, 'confidence'),
crc32: saefInt(entry, 'crc32'),
hasparity: entry.getAttribute('hasparity') || undefined,
id: saefInt(entry, 'id'),
npar: saefInt(entry, 'npar'),
stride: saefInt(entry, 'stride'),
syndrome: entry.getAttribute('syndrome') || undefined,
toc: entry.hasAttribute('toc') ?
entry.getAttribute('toc').split(':').map(offset => parseInt(offset)) : undefined,
trackcrcs: entry.hasAttribute('trackcrcs') ?
entry.getAttribute('trackcrcs').split(' ').map(crc => parseInt(crc, 16)) : undefined,
})),
}));
}).then(function(results) {
console.log('CTDB lookup (%s, %d) results:', params.metadata, params.fuzzy, results);
return results.length > 0 && results[0] != null && (results = Object.assign(results[0].metadata.filter(function(metadata) {
if (!['musicbrainz', 'discogs'].includes(metadata.source)) return false;
if (metadata.discCount > 0 && metadata.discCount != results.length) return false;
return true;
}), { confidence: (entries => getSessions(torrentId).then(sessions => sessions.length == entries.length ? sessions.map(function(session, volumeNdx) {
if (rxRangeRip.test(session)) return null;
const rx = [
/^\s+(?:(?:Copy|复制|Kopie|Copia|Kopiera|Copiar|Копиран) CRC|CRC (?:копии|êîïèè|kopii|kopije|kopie|kópie)|コピーCRC)\s+([\da-fA-F]{8})$/gm,
/^\s+(?:CRC32 hash|CRC)\s*:\s*([\da-fA-F]{8})$/gm, // XLD / EZ CD
];
return (session = session.match(rx[0]) || session.match(rx[1])) && session.map(match =>
parseInt(rx.reduce((m, rx) => m || (rx.lastIndex = 0, rx.exec(match)), null)[1], 16));
}).map(function getScores(checksums, volumeNdx) {
if (checksums == null || entries[volumeNdx] == null || checksums.length < 3
|| !entries[volumeNdx].some(entry => entry.trackcrcs.length == checksums.length)) return null; // tracklist too short
const getMatches = matchFn => entries[volumeNdx].reduce((sum, entry, ndx) =>
matchFn(entry.trackcrcs.length == checksums.length ? entry.trackcrcs.slice(1, -1) .filter((crc32, ndx) =>
crc32 == checksums[ndx + 1]).length / (entry.trackcrcs.length - 2) : -Infinity) ?
sum + entry.confidence : sum, 0);
return [entries[volumeNdx].reduce((sum, entry) => sum + entry.confidence, 0),
getMatches(score => score >= 1), getMatches(score => score >= 0.5), getMatches(score => score > 0)];
}) : Promise.reject('assertion failed: LOGfiles miscount')).then(function getTotal(scores) {
if ((scores = scores.filter(Boolean)).length <= 0)
return Promise.reject('all media having too short tracklist, mismatching tracklist length, range rip or failed to extract checksums');
const sum = array => array.reduce((sum, val) => sum + val, 0);
const getTotal = index => Math.min(...(index = scores.map(score => score[index]))) > 0 ? sum(index) : 0;
return {
matched: getTotal(1),
partiallyMatched: getTotal(2),
anyMatched: getTotal(3),
total: sum(scores.map(score => score[0])),
};
}))(results.map(result => result && result.entries)) })).length > 0 ? results : Promise.reject('No matches');
});
const methods = [
{ metadata: 'fast', fuzzy: 0 }, { metadata: 'default', fuzzy: 0 }, { metadata: 'extensive', fuzzy: 0 },
{ metadata: 'fast', fuzzy: 1 }, { metadata: 'default', fuzzy: 1 }, { metadata: 'extensive', fuzzy: 1 },
];
target.textContent = 'Looking up...';
target.style.color = null;
(function execMethod(index = 0, reason = 'index out of range') {
return index < methods.length ? ctdbLookup(methods[index]).then(results =>
Object.assign(results, { method: methods[index] }),
reason => execMethod(index + 1, reason)) : Promise.reject(reason);
})().then(function(results) {
target.textContent = `${results.length}${Boolean(results.method.fuzzy) ? ' fuzzy' : ''} ${results.method.metadata} ${results.length == 1 ? 'match' : 'matches'}`;
target.style.color = '#' + (['fast', 'default', 'extensive'].indexOf(results.method.metadata) +
results.method.fuzzy * 3 << 1).toString(16) + 'a0';
return queryAjaxAPICached('torrent', { id: torrentId }).then(function(torrent) {
const isCompleteInfo = torrent.torrent.remasterYear > 0
&& Boolean(torrent.torrent.remasterRecordLabel)
&& Boolean(torrent.torrent.remasterCatalogueNumber);
const is = what => !torrent.torrent.remasterYear && {
unknown: torrent.torrent.remastered,
unconfirmed: !torrent.torrent.remastered,
}[what];
let [method, confidence] = [results.method, results.confidence];
const confidenceBox = document.createElement('SPAN');
confidence.then(function(confidence) {
if (confidence.anyMatched <= 0) return Promise.reject('mismatch');
let color = confidence.matched || confidence.partiallyMatched || confidence.anyMatched;
color = Math.round(color * 0x55 / confidence.total);
color = 0x55 * (3 - Number(confidence.partiallyMatched > 0) - Number(confidence.matched > 0)) - color;
confidenceBox.innerHTML = svgCheckmark('1em', '#' + (color << 16 | 0xCC00).toString(16).padStart(6, '0'));
confidenceBox.className = confidence.matched > 0 ? 'ctdb-verified' : 'ctdb-partially-verified';
setTooltip(confidenceBox, `Checksums${confidence.matched > 0 ? '' : ' partially'} matched (confidence ${confidence.matched || confidence.partiallyMatched || confidence.anyMatched}/${confidence.total})`);
}).catch(function(reason) {
confidenceBox.innerHTML = reason == 'mismatch' ? svgFail('1em') : svgQuestionmark('1em');
confidenceBox.className = 'ctdb-not-verified';
setTooltip(confidenceBox, `Could not verify checksums (${reason})`);
}).then(() => { target.parentNode.append(confidenceBox) });
confidence = confidence.then(confidence =>
is('unknown') && confidence.anyMatched <= 0 ? Promise.reject('mismatch') : confidence,
reason => ({ matched: undefined, partiallyMatched: undefined, anyMatched: undefined }));
const _getReleaseYear = metadata => (metadata = metadata.release.map(release => getReleaseYear(release.date)))
.every((year, ndx, arr) => year > 0 && year == arr[0]) ? metadata[0] : NaN;
const labelInfoMapper = metadata => metadata.labelInfo.map(labelInfo => ({
label: metadata.source == 'discogs' ? stripNameSuffix(labelInfo.name) : labelInfo.name,
catNo: labelInfo.catno,
})).filter(labelInfo => labelInfo.label || labelInfo.catNo);
// In-page results table
const [thead, table, tbody] = ['DIV', 'TABLE', 'TBODY'].map(Document.prototype.createElement.bind(document));
thead.style = 'margin-bottom: 5pt;';
thead.innerHTML = `<b>Applicable CTDB matches</b> (method: ${Boolean(method.fuzzy) ? 'fuzzy, ' : ''}${method.metadata})`;
table.style = 'margin: 0; max-height: 10rem; overflow-y: auto; empty-cells: hide;';
table.className = 'ctdb-lookup-results ctdb-lookup-' + torrentId;
tbody.dataset.torrentId = torrentId; tbody.dataset.edition = target.parentNode.dataset.edition;
results.forEach(function(metadata) {
const [tr, source, artist, title, release, editionInfo, barcode, relevance] =
['TR', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD', 'TD'].map(Document.prototype.createElement.bind(document));
tr.className = 'ctdb-metadata';
tr.style = 'word-wrap: break-word; transition: color 200ms ease-in-out;';
tr.dataset.url = {
musicbrainz: 'https://musicbrainz.org/release/' + metadata.id,
discogs: dcOrigin + '/release/' + metadata.id,
}[metadata.source];
[release, barcode, relevance].forEach(elem => { elem.style.whiteSpace = 'nowrap' });
[relevance].forEach(elem => { elem.style.textAlign = 'right' });
if (source.innerHTML = GM_getResourceText({ musicbrainz: 'mb_logo', discogs: 'dc_icon' }[metadata.source])) {
source.firstElementChild.removeAttribute('width');
source.firstElementChild.setAttribute('height', '1em');
svgSetTitle(source.firstElementChild, metadata.source);
} else source.innerHTML = `<img src="http://s3.cuetools.net/icons/${metadata.source}.png" height="12" title="${metadata.source}" />`;
artist.textContent = metadata.source == 'discogs' ? stripNameSuffix(metadata.artist) : metadata.artist;
title.innerHTML = linkHTML(tr.dataset.url, metadata.album, metadata.source + '-release');
release.innerHTML = metadata.release.map(release => releaseToHtml(release)).filter(Boolean).join('<br>');
editionInfo.innerHTML = metadata.labelInfo.map(labelInfo => [
labelInfo.name && `<span class="label">${stripNameSuffix(labelInfo.name)}</span>`,
labelInfo.catno && `<span class="catno" style="white-space: nowrap;">${labelInfo.catno}</span>`,
].filter(Boolean).join(' ')).filter(Boolean).join('<br>');
if (editionInfo.childElementCount <= 0 && metadata.source == 'musicbrainz')
mbFindEditionInfoInAnnotation(editionInfo, metadata.id);
if (metadata.barcode) barcode.textContent = metadata.barcode;
if (metadata.relevance >= 0) {
relevance.textContent = metadata.relevance + '%';
relevance.title = 'Relevance';
}
(!isCompleteInfo && 'edition' in target.parentNode.dataset && !Boolean(method.fuzzy)
&& !noEditPerms && (editableHosts.includes(document.domain) || ajaxApiKey)
&& (!is('unknown') || method.metadata != 'extensive' || !(metadata.relevance < 100)) ?
confidence : Promise.reject('Not applicable')).then(function(confidence) {
const releaseYear = _getReleaseYear(metadata);
if (!(releaseYear > 0)) return Promise.reject('Unknown or inconsistent release year');
const editionInfo = labelInfoMapper(metadata);
if (editionInfo.length <= 0 && !metadata.barcode && torrent.torrent.remasterYear > 0)
return Promise.reject('No additional edition information');
tr.dataset.releaseYear = releaseYear;
if (editionInfo.length > 0) tr.dataset.editionInfo = JSON.stringify(editionInfo);
if (metadata.barcode) tr.dataset.barcodes = JSON.stringify([ metadata.barcode ]);
if (!torrent.torrent.description.includes(metadata.id))
tr.dataset.description = torrent.torrent.description.trim();
applyOnClick(tr);
}).catch(reason => { openOnClick(tr) });
tr.append(source, artist, title, release, editionInfo, barcode, relevance);
['source', 'artist', 'title', 'release-events', 'edition-info', 'barcode', 'relevance']
.forEach((className, index) => tr.cells[index].className = className);
tbody.append(tr);
});
table.append(tbody);
addLookupResults(torrentId, thead, table);
// Group set
if (isCompleteInfo || !('edition' in target.parentNode.dataset) || Boolean(method.fuzzy)
|| noEditPerms || !editableHosts.includes(document.domain) && !ajaxApiKey
|| torrent.torrent.remasterYear > 0 && !(results = results.filter(metadata =>
isNaN(metadata = _getReleaseYear(metadata)) || metadata == torrent.torrent.remasterYear))
.some(metadata => metadata.labelInfo && metadata.labelInfo.length > 0 || metadata.barcode)
|| results.length > (is('unknown') ? 1 : 3)
|| is('unknown') && method.metadata == 'extensive' && results.some(metadata => metadata.relevance < 100))
return;
confidence.then(function(confidence) {
const releaseYear = results.reduce((year, metadata) => isNaN(year) ? NaN :
(metadata = _getReleaseYear(metadata)) > 0 && (year <= 0 || metadata == year) ? metadata : NaN, -Infinity);
if (!(releaseYear > 0) || !results.every(m1 => m1.release.every(r1 => results.every(m2 =>
m2.release.every(r2 => getReleaseYear(r2.date) == getReleaseYear(r1.date)))))) return;
const a = document.createElement('A');
a.className = 'update-edition';
a.href = '#';
a.textContent = '(set)';
if (results.length > 1 || results.some(result => result.relevance < 100)
|| !(confidence.partiallyMatched > 0)) {
a.style.fontWeight = 300;
a.dataset.confirm = true;
} else a.style.fontWeight = 'bold';
a.dataset.releaseYear = releaseYear;
const editionInfo = Array.prototype.concat.apply([ ], results.map(labelInfoMapper));
if (editionInfo.length > 0) a.dataset.editionInfo = JSON.stringify(editionInfo);
const barcodes = results.map(metadata => metadata.barcode).filter(Boolean);
if (barcodes.length > 0) a.dataset.barcodes = JSON.stringify(barcodes);
if (results.length < 2 && !torrent.torrent.description.includes(results[0].id)) {
a.dataset.description = torrent.torrent.description.trim();
a.dataset.url = {
musicbrainz: mbOrigin + '/release/' + results[0].id,
discogs: dcOrigin + '/release/' + results[0].id,
}[results[0].source];
}
setTooltip(a, 'Update edition info from matched release(s)\n\n' + results.map(metadata =>
metadata.labelInfo.map(labelInfo => ({
discogs: 'Discogs',
musicbrainz: 'MusicBrainz',
}[metadata.source]) + ' ' + [
_getReleaseYear(metadata),
[stripNameSuffix(labelInfo.name), labelInfo.catno || metadata.barcode].filter(Boolean).join(' / '),
].filter(Boolean).join(' - ') + (metadata.relevance >= 0 ? ` (${metadata.relevance}%)` : ''))
.filter(Boolean).join('\n')).join('\n'));
a.onclick = updateEdition;
target.parentNode.append(a);
});
}, alert);
}, function(reason) {
target.textContent = reason;
target.style.color = 'red';
}).then(() => { target.parentNode.dataset.haveQuery = true });
}, 'Lookup edition in CUETools DB (TOCID)');
}
let elem = document.body.querySelector('div#discog_table > div.box.center > a:last-of-type');
if (elem != null) {
const a = document.createElement('A'), captions = ['Incomplete editions only', 'All editions'];
a.textContent = captions[0];
a.href = '#';
a.className = 'brackets';
a.style.marginLeft = '2rem';
a.onclick = function(evt) {
if (captions.indexOf(evt.currentTarget.textContent) == 0) {
for (let strong of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.edition.discog > td.edition_info > strong')) (function(tr, show = true) {
if (show) (function(tr) {
show = false;
while ((tr = tr.nextElementSibling) != null && tr.classList.contains('torrent_row')) {
const a = tr.querySelector('td > a:last-of-type');
if (a == null || !/\bFLAC\s*\/\s*Lossless\s*\/\s*Log\s*\(\-?\d+%\)/.test(a.textContent)) continue;
show = true;
break;
}
})(tr);
if (show) (function(tr) {
while (tr != null && !tr.classList.contains('group')) tr = tr.previousElementSibling;
if (tr != null && (tr = tr.querySelector('div > a.show_torrents_link')) != null
&& tr.parentNode.classList.contains('show_torrents')) tr.click();
})(tr); else (function(tr) {
do tr.hidden = true;
while ((tr = tr.nextElementSibling) != null && tr.classList.contains('torrent_row'));
})(tr);
})(strong.parentNode.parentNode, incompleteEdition.test(strong.lastChild.textContent.trim()));
for (let tr of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.group.discog')) (function(tr) {
if (!(function(tr) {
while ((tr = tr.nextElementSibling) != null && !tr.classList.contains('group'))
if (tr.classList.contains('edition') && !tr.hidden) return true;
return false;
})(tr)) tr.hidden = true;
})(tr);
} else for (let tr of document.body.querySelectorAll('div#discog_table > table.torrent_table.grouped > tbody > tr.discog'))
tr.hidden = false;
evt.currentTarget.textContent = captions[1 - captions.indexOf(evt.currentTarget.textContent)];
};
elem.after(a);
}
}