Greasy Fork is available in English.
Auto sets track lengths for media by unique attached disc id.
当前为
// ==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 http://greasyfork.icu/users/321857
// @copyright 2024, Anakunda (http://greasyfork.icu/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
// ==/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);