// ==UserScript== // @name MB Auto Track Lengths // @version 1.02 // @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; 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 = link.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 }); 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) { 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/')) { 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('sel')) return Promise.reject('Disc ids is current tab'); if (li.classList.contains('disabled')) return Promise.reject('Release has no disc ids attached'); return localXHR(tabLink).then(document => processDocument(document, autoSet ? 2 : 1)).then(function(statuses) { if (statuses.some(status => status < 0)) li.style.backgroundColor = '#f002'; else if (statuses.some(status => status > 2)) li.style.backgroundColor = '#0f02'; if (statuses.some(status => status == 2)) li.style.fontWeight = 'bold'; li.title = statuses; return statuses; }, function(reason) { li.style.backgroundColor = '#f004'; li.title = reason; return Promise.reject(reason); }); })(tabLinks[0]); } else if (document.location.pathname.startsWith('/cdtoc/')) processDocument(document);