Greasy Fork

MB Auto Track Lengths

Auto sets track lengths for media by unique attached disc id.

目前为 2024-05-02 提交的版本。查看 最新版本

// ==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
// ==/UserScript==

'use strict';

const autoSet = false;

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 diffs = 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[diffs.some(diff => diff > 5) ? 'reject' : 'resolve'](diffs);
			}).then(function(diffs) {
				if (mode == 1) return 1;
				const postData = new URLSearchParams({ 'confirm.edit_note': '' });
				if (makeVotable) postData.set('confirm.make_votable', 1);
				return localXHR(link, { responseType: null }, postData).then(function(statusCode) {
					link.replaceWith(Object.assign(document.createElement('span'), {
						textContent: 'Track lengths successfully set',
						style: 'color: green;',
						title: 'Status code: ' + statusCode + '\nDiffs: ' + diffs,
					}));
					return 2;
				}, 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(diff => diff > 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 0;
		} 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 > 1)) li.style.backgroundColor = '#0f02';
			else if (statuses.some(status => status > 0)) 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);