// ==UserScript== // @name MB Auto Track Lengths // @version 1.03 // @match https://musicbrainz.org/release/* // @match https://beta.musicbrainz.org/release/* // @match https://musicbrainz.org/cdtoc/* // @match https://beta.musicbrainz.org/cdtoc/* // @run-at document-end // @author Anakunda // @namespace https://greasyfork.org/users/321857 // @copyright 2024, Anakunda (https://greasyfork.org/users/321857) // @license GPL-3.0-or-later // @description Auto sets track lengths for media by unique attached disc id. // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js // @downloadURL none // ==/UserScript== 'use strict'; const autoSet = true; const flashElement = elem => elem instanceof HTMLElement ? elem.animate([ { offset: 0.0, opacity: 1 }, { offset: 0.4, opacity: 1 }, { offset: 0.5, opacity: 0.1 }, { offset: 0.9, opacity: 0.1 }, ], { duration: 600, iterations: Infinity }) : null; function processDocument(document, mode = 2) { function getRequestparams(link) { console.assert(link instanceof HTMLAnchorElement); if (!(link instanceof HTMLAnchorElement)) throw 'Invalid argument'; const params = { discId: /^\/cdtoc\/([\w\_\.\-]+)\/set-durations$/i.exec(link.pathname) }; if (params.discId != null) params.discId = params.discId[1]; else return null; const query = new URLSearchParams(link.search); params.mediumId = parseInt(query.get('medium')); return params.mediumId >= 0 ? params : null; } const isDiscIdRow = row => row instanceof HTMLElement && ['odd', 'even'].some(cls => row.classList.contains(cls)); const getSetLink = row => Array.prototype.find.call(row.querySelectorAll('td a'), function(a) { //if (a.textContent.trim() == 'Set track lengths') return true; return getRequestparams(a) != null; }) || null; let rows = document.body.querySelectorAll('table.tbl > tbody > tr.subh'), groups = [ ]; if (rows.length > 0) rows.forEach(function(row) { const discIds = [ ]; while (isDiscIdRow(row = row.nextElementSibling)) discIds.push(getSetLink(row)); groups.push(discIds); }); else { rows = document.body.querySelectorAll('table.tbl > tbody > tr'); groups = Array.prototype.filter.call(rows, isDiscIdRow).map(getSetLink); } return groups.length > 0 ? Promise.all(groups.map(function(group) { function setTrackLengths(link, makeVotable = false) { console.assert(link instanceof HTMLAnchorElement); if (!(link instanceof HTMLAnchorElement)) throw 'Invalid argument'; if (link.disabled) return Promise.resolve(0); else link.disabled = true; const animation = flashElement(link); return localXHR(link).then(function(document) { const deltas = Array.from(document.body.querySelectorAll('div#page > table.wrap-block.details'), function(track) { const times = ['old', 'new'].map(function(cls) { if ((cls = track.querySelector('td.' + cls)) != null) cls = cls.textContent; else return NaN; return cls.split(':').reverse().reduce((total, time, index) => total + parseInt(time) * Math.pow(60, index), 0); }); return Math.abs(times[1] - times[0]); }); return Promise[deltas.some(delta => delta > 5) ? 'reject' : 'resolve'](deltas); }).then(function(deltas) { if (mode == 1) return 2; const postData = new URLSearchParams({ 'confirm.edit_note': '' }); if (makeVotable) postData.set('confirm.make_votable', 1); return localXHR(link, { responseType: null }, postData).then(function(statusCode) { let title = 'Status code: ' + statusCode; if (deltas.length > 0) title = 'Deltas: ' + deltas + '\n' + title; link.replaceWith(Object.assign(document.createElement('span'), { textContent: 'Track lengths successfully set', style: 'color: green;', title: title, })); return 3; }, function(reason) { link.replaceWith(Object.assign(document.createElement('span'), { textContent: 'Error setting track lengths', style: 'color: red;', title: reason, })); return -100; }); }, function(reason) { if (animation != null) animation.cancel(); link.style.color = 'red'; link.disabled = false; link.title = reason; return Array.isArray(reason) ? reason.some(delta => delta > 30) ? -2 : -1 : -100; }); } if (!(mode > 0) || group.length > 1) { for (let link of group.filter(Boolean)) link.onclick = function(evt) { setTrackLengths(link = evt.currentTarget) .then(status => { if (status < 0) document.location.assign(link) }); return false; }; return 1; } else if (group.length > 0) group = group.filter(Boolean); return group.length == 1 ? setTrackLengths(group[0]) : 0; })) : Promise.reject('No disc ids found'); } if (document.location.pathname.startsWith('/release/')) { function addReleaseId(releaseId) { if (!releaseId || scannedReleaseIds.includes(releaseId)) return; scannedReleaseIds.push(releaseId); sessionStorage.setItem('scanned_discids', JSON.stringify(scannedReleaseIds)); } let scannedReleaseIds = [ ]; if ('scanned_discids' in sessionStorage) try { scannedReleaseIds = JSON.parse(sessionStorage.getItem('scanned_discids')); } catch(e) { console.warn(e) } let releaseId = /^\/release\/([a-f\d\-]+)/i.exec(document.location.pathname); releaseId = releaseId != null ? releaseId[1] : undefined; console.assert(releaseId, document.location.pathname); if (releaseId && scannedReleaseIds.includes(releaseId)) return; if (document.location.pathname.endsWith('/discids')) processDocument(document); let tabLinks = document.body.querySelectorAll('div#page ul.tabs > li a'); tabLinks = Array.prototype.filter.call(tabLinks, a => a.href.endsWith('/discids')); console.assert(tabLinks.length <= 1, tabLinks); if (tabLinks.length == 1) (function processPage(tabLink) { const li = tabLink.closest('li'); console.assert(li != null); if (li.classList.contains('disabled')) { addReleaseId(releaseId); return Promise.reject('Release has no disc ids attached'); } if (li.classList.contains('sel')) { addReleaseId(releaseId); return Promise.reject('Disc ids is current tab'); } const animation = flashElement(tabLink); return localXHR(tabLink).then(document => processDocument(document, autoSet ? 2 : 1)).then(function(statuses) { if (animation != null) animation.cancel(); const titles = [ ]; if (statuses.some(status => status == 3)) { li.style.backgroundColor = '#0f02'; titles.push('Track lengths successfully applied'); } if (statuses.some(status => status < 0)) { li.style.backgroundColor = '#f002'; titles.push('Timing too different or network error'); } if (statuses.some(status => status == 2)) { li.style.fontWeight = 'bold'; titles.push('CD TOC available to apply'); } if (statuses.some(status => status == 1)) titles.push('Ambiguity: multiple TOCs attached to medium'); if (statuses.some(status => status == 0)) titles.push('TOC already applied'); if (titles.length <= 0) titles.push(String(statuses)); li.title = titles.join('\n'); if (statuses.every(status => status >= 0)) addReleaseId(releaseId); return statuses; }, function(reason) { if (animation != null) animation.cancel(); li.style.backgroundColor = '#f004'; li.title = reason; return Promise.reject(reason); }); })(tabLinks[0]); else addReleaseId(releaseId); } else if (document.location.pathname.startsWith('/cdtoc/')) processDocument(document);