For me it works like a charm (what a time saver). After using the “guess work” script I have to save once and then I can use the “Recording-Work relation attributes”-Script for setting the live attribute.
I do this. I wish there was a button to just reuse existing recordings from a different release in the same release group.
If I select the duplicate first, I find I sometimes miss minor changes (or duplicate mistakes/typos etc) between the two. I like to be able to compare the two sources in the recordings comparison screen.
Basically, reusing/picking existing recordings from the same group is more or less what this script is made to do. I haven’t tested exhaustively but it seems to do okay. Definitely better than nothing!
My current workflow, copy tracks from new import, go back a step and use the duplicate option, parse tracks and paste the new copied tracklist, and then the recording tab matches all existing tracks already (even when track titles are totally different scripts..)
This isn’t a good solution when the track is in a different script than the original release.
That’s what I did too!
You are correct that the auto-picker script wont help with translations and similar. You could probably feed AI @jesus2099’s mass-merge recording script to get that functionality, though.
I was going to ask if “AI Bob” properly credits their sources. Would seem a logical thing to do. And then I looked at your script. Bob leaves an error in the headers. Look at Download URL and Update URL. Shouldn’t they point at this new script?
// ==UserScript==
// @name MusicBrainz seed artist relationships from Linktr.ee
// @namespace https://github.com/Aerozol/metabrainz-userscripts
// @description Seed MusicBrainz artist URL relationships from Linktr.ee
// @author Gemini (thanks to loujine's scripts)
// @version 0.7
// @downloadURL https://raw.githubusercontent.com/loujine/musicbrainz-scripts/master/mb-edit-create_from_wikidata.user.js
// @updateURL https://raw.githubusercontent.com/loujine/musicbrainz-scripts/master/mb-edit-create_from_wikidata.user.js
// @license MIT
// @require https://raw.githubusercontent.com/loujine/musicbrainz-scripts/master/mbz-loujine-common.js
// @include http*://*musicbrainz.org/artist/*/edit
// @grant GM_xmlhttpRequest
// @connect linktr.ee
// @run-at document-end
// ==/UserScript==
I know little of these scripts, but do sometimes look at the headers of @jesus2099 and @loujin scripts to check where they came from. I think your headers are missing their Download URLs and Update links?
Also a user bug report. Is your new script supposed to be alive when I do an import from Discogs? It seemed to go AWOL yesterday when I wanted to test it. Maybe just something I did? But would be useful to be alive during the import.
Thanks @IvanDobsky! I had forgotten to change the download and update headers - I’ve updated the other scripts to include them as well.
A release import?
It currently is only intended to run on: http*://*musicbrainz.org/artist/*/edit
If you muck with that line, or add more, you could see if it works elsewhere as well (but you’ll have to bug check, sorry!)
AI Bob is a bastard and hates giving credit… I added in the extra thanks to loujine.
So a little hint to other people - they need to manually update to get the updated update headers with the how to update automatically bit included otherwise they’ll never get an update.
Okay… may try if feeling brave… will report any findings.
Isn’t that just following industry standards? Find someone else’s code, copy, adjust, delete author, profit.
The “Quick Recording Match” script is very interesting. @aerozol do you know the “Release Seeding Helper” script by @Anakunda?
I attempted to “marry” them. Your script now looks at the visual cues if available to make its choices. Very briefly tested.
// ==UserScript==
// @name MusicBrainz Quick Recording Match
// @namespace https://musicbrainz.org/user/chaban
// @version 5.23
// @tag ai-created
// @description Select the first recording search result for each track, in the release editor Recordings tab.
// @author chaban, Aerozol
// @license MIT
// @match *://*.musicbrainz.org/release/*/edit*
// @match *://*.musicbrainz.org/release/add*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_NAME = GM.info.script.name;
let isCancelled = false;
let currentIndex = 0;
let editButtons;
let mainButtons;
let currentTrackRow = null;
let ignoredConfidenceLevel = 'none';
let matchingMethod = 'suggested';
const confidenceColors = {
'yellow': '#fff176',
'orange': '#ffc778',
'dark-orange': '#ffb74d',
'red': '#d32f2f',
};
function parseLengthToMs(lengthText) {
const match = lengthText.match(/(\d+):(\d+)/);
if (match) {
const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);
return (minutes * 60 + seconds) * 1000;
}
return null;
}
function getTrackComparisonData(trackRow) {
const artistRow = trackRow.nextElementSibling;
const nameCells = trackRow.querySelectorAll('td.name');
const lengthCells = trackRow.querySelectorAll('td.length');
if (nameCells.length < 2 || lengthCells.length < 2 || !artistRow) {
return null;
}
const recordingTitleCell = nameCells[1];
const recordingTitle = recordingTitleCell?.querySelector('bdi')?.textContent.trim().toLowerCase() ?? '';
if (!recordingTitle || recordingTitleCell.classList.contains('add-new')) {
return { confidence: null, differences: [] };
}
const trackTitle = nameCells[0]?.querySelector('bdi')?.textContent.trim().toLowerCase() ?? '';
const trackArtists = artistRow?.querySelector('td[colspan="2"]:first-of-type > span')?.textContent.trim().toLowerCase() ?? '';
const recordingArtists = artistRow?.querySelector('td[colspan="2"]:last-of-type > span')?.textContent.trim().toLowerCase() ?? '';
const trackLengthMs = parseLengthToMs(lengthCells[0].textContent.trim());
const recordingLengthMs = parseLengthToMs(lengthCells[1].textContent.trim());
let lengthDiff = 0;
if (trackLengthMs !== null && recordingLengthMs !== null) {
lengthDiff = Math.abs(trackLengthMs - recordingLengthMs);
}
const differences = [];
if (trackTitle !== recordingTitle) {
differences.push('Title');
}
if (trackArtists !== recordingArtists) {
differences.push('Artist');
}
if (lengthDiff > 1000) {
differences.push(`Length (${Math.round(lengthDiff / 1000)}s)`);
}
let confidence = null;
if (differences.length >= 3 && lengthDiff > 10000) {
confidence = 'red';
} else if (lengthDiff > 15000) {
confidence = 'dark-orange';
} else if (differences.length >= 2 && lengthDiff <= 15000) {
confidence = 'orange';
} else if (differences.length === 1 || (lengthDiff > 3000 && lengthDiff <= 15000)) {
confidence = 'yellow';
}
return { differences, confidence };
}
function getConfidenceLevel(trackRow) {
const data = getTrackComparisonData(trackRow);
return data ? data.confidence : null;
}
function highlightSingleTrack(trackRow) {
if (!trackRow) return;
const editButton = trackRow.querySelector('.edit-track-recording');
if (!editButton) return;
editButton.style.backgroundColor = '';
editButton.title = '';
if (trackRow.querySelector('.edit-track-recording.negative')) {
return;
}
const data = getTrackComparisonData(trackRow);
if (data && data.confidence) {
const tooltipText = data.differences.length > 0
? `${data.differences.join(', ')} ${data.differences.length > 1 ? 'differences' : 'difference'}`
: '';
editButton.style.backgroundColor = confidenceColors[data.confidence];
editButton.title = tooltipText;
}
}
function highlightAllDifferences() {
const trackRows = document.querySelectorAll('#track-recording-assignation tr.track');
trackRows.forEach(highlightSingleTrack);
}
function createButton(text, onClickHandler) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = text;
button.className = 'musicbrainz-quick-tool-button';
button.style.cssText = `
font-size: 1em; padding: 4px 10px; cursor: pointer;
background-color: #f8f8f8; border: 1px solid #ddd;
border-radius: 4px; margin-right: 5px;
`;
button.onclick = onClickHandler;
return button;
}
function createDropdown(id, labelText, options) {
const container = document.createElement('span');
container.style.display = 'inline-flex';
container.style.alignItems = 'center';
const separator = document.createElement('span');
separator.textContent = '|';
separator.style.cssText = 'margin: 0 10px 0 5px; color: #ccc;';
container.appendChild(separator);
const label = document.createElement('span');
label.textContent = labelText;
label.style.cssText = 'font-weight: bold; margin-right: 5px;';
container.appendChild(label);
const select = document.createElement('select');
select.id = id;
select.style.cssText = `
padding: 4px; border: 1px solid #ddd; border-radius: 4px; font-size: 1em;
`;
for (const [value, text] of Object.entries(options)) {
const option = document.createElement('option');
option.value = value;
option.textContent = text;
select.appendChild(option);
}
return { container, select };
}
function createConfidenceDropdown() {
const options = {
'none': 'Nothing',
'yellow_and_above': 'Low confidence',
'orange_and_above': 'Very low confidence',
'red': 'Extremely low confidence',
};
const { container, select } = createDropdown('confidence-level-dropdown', 'Ignore:', options);
select.addEventListener('change', (event) => {
ignoredConfidenceLevel = event.target.value;
});
container.appendChild(select);
return container;
}
function createMethodDropdown() {
const options = {
'suggested': 'First suggested recording',
'search': 'First search result',
};
const { container, select } = createDropdown('matching-method-dropdown', 'Method:', options);
select.addEventListener('change', (event) => {
matchingMethod = event.target.value;
});
container.appendChild(select);
return container;
}
function createButtonContainer() {
const fieldset = document.createElement('fieldset');
fieldset.className = 'quick-tools-fieldset';
const legend = document.createElement('legend');
legend.textContent = 'Quick tools';
fieldset.appendChild(legend);
const p1 = document.createElement('p');
p1.appendChild(createButton('Auto-link all tracks', () => startAutoLinking('Auto-linking')));
p1.appendChild(createButton('Unlink all tracks', () => startUnlinking('Unlinking')));
p1.appendChild(createConfidenceDropdown());
p1.appendChild(createMethodDropdown());
fieldset.appendChild(p1);
return fieldset;
}
function addQuickToolsButtons() {
const targetDiv = document.querySelector('div[data-bind="affectsBubble: $root.recordingBubble"]');
if (targetDiv && !document.querySelector('.quick-tools-fieldset')) {
targetDiv.before(createButtonContainer());
}
}
function removeQuickToolsButtons() {
document.querySelector('.quick-tools-fieldset')?.remove();
}
function createCancelButton() {
const cancelButton = document.createElement('button');
cancelButton.id = 'quick-tools-cancel-button';
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
position: fixed; bottom: 20px; right: 20px; padding: 10px 20px;
font-size: 16px; background-color: #d32f2f; color: white;
border: none; border-radius: 5px; cursor: pointer; z-index: 1000;
`;
cancelButton.onclick = cancelProcess;
document.body.appendChild(cancelButton);
}
function removeCancelButton() {
document.getElementById('quick-tools-cancel-button')?.remove();
}
function setControlsEnabled(enabled) {
mainButtons = document.querySelectorAll('.musicbrainz-quick-tool-button');
mainButtons.forEach(button => {
button.disabled = !enabled;
button.style.opacity = enabled ? '1' : '0.5';
button.style.cursor = enabled ? 'pointer' : 'not-allowed';
});
const dropdowns = document.querySelectorAll('#confidence-level-dropdown, #matching-method-dropdown');
dropdowns.forEach(dropdown => {
if (dropdown) dropdown.disabled = !enabled;
});
}
function cancelProcess() {
isCancelled = true;
removeCancelButton();
setControlsEnabled(true);
console.log(`%c${SCRIPT_NAME}: 🛑 Process cancelled by user.`, 'font-weight: bold; color: orange;');
}
function runProcess(handler, processName) {
isCancelled = false;
currentIndex = 0;
editButtons = document.querySelectorAll('#recordings .edit-track-recording');
if (editButtons.length === 0) {
alert("No tracks found. Make sure you are on the 'Recordings' tab and have a tracklist.");
return;
}
console.log(`%c${SCRIPT_NAME}: ▶️ Starting ${processName} process for ${editButtons.length} tracks.`, 'font-weight: bold;');
setControlsEnabled(false);
createCancelButton();
handler(processName);
}
function shouldIgnore(confidence) {
const levels = ['yellow', 'orange', 'dark-orange', 'red'];
const ignoredLevels = {
'red': ['red'],
'orange_and_above': ['orange', 'dark-orange', 'red'],
'yellow_and_above': levels,
}[ignoredConfidenceLevel];
return ignoredLevels?.includes(confidence) ?? false;
}
function isGoodMatch(element) {
if (!element) return false;
const bgColor = window.getComputedStyle(element).backgroundColor;
return bgColor && (bgColor.startsWith('rgb(0, 255, 0') || bgColor.startsWith('rgba(0, 255, 0'));
}
function findBestMatchInput() {
const suggestedRows = document.querySelectorAll('#recording-assoc-bubble table > tbody > tr');
for (const row of suggestedRows) {
const input = row.querySelector('td.select input[type="radio"]');
if (!input) continue;
const titleEl = row.querySelector('td.recording bdi');
const artistLinks = row.querySelectorAll('td.artist a');
const lengthEl = row.querySelector('td.length span');
const isTitleMatch = isGoodMatch(titleEl);
const isArtistMatch = artistLinks.length > 0 && Array.from(artistLinks).every(isGoodMatch);
const isLengthMatch = isGoodMatch(lengthEl);
if (isTitleMatch && isArtistMatch && isLengthMatch) {
return input;
}
}
return null;
}
function startAutoLinking(processName) {
const autoLinkButton = document.querySelector('.musicbrainz-quick-tool-button');
autoLinkButton.style.backgroundColor = '#ebbba0';
runProcess(() => {
processNextTrack('Auto-linking');
}, processName);
}
function startUnlinking(processName) {
const unlinkButton = document.querySelectorAll('.musicbrainz-quick-tool-button')[1];
unlinkButton.style.backgroundColor = '#ebbba0';
runProcess(() => {
processNextTrack('Unlinking');
}, processName);
}
function processNextTrack(processName) {
if (isCancelled || currentIndex >= editButtons.length) {
if (!isCancelled) {
console.log(`%c${SCRIPT_NAME}: ✅ ${processName} process finished successfully.`, 'font-weight: bold; color: green;');
document.querySelector('.musicbrainz-quick-tool-button').style.backgroundColor = '#bceba0';
highlightAllDifferences();
}
removeCancelButton();
setControlsEnabled(true);
return;
}
const currentButton = editButtons[currentIndex];
currentTrackRow = currentButton.closest('tr.track');
const trackName = currentTrackRow.querySelector('td.name bdi')?.textContent.trim();
console.groupCollapsed(`Track ${currentIndex + 1}/${editButtons.length}: "${trackName}"`);
currentButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log("🖱️ Clicking the 'Edit' button.");
currentButton.click();
setTimeout(() => {
if (processName === 'Auto-linking') {
handleAutoLinkSelection();
} else if (processName === 'Unlinking') {
handleUnlinkSelection();
}
}, 1000);
}
function proceedToNext(processName) {
const nextButton = document.querySelector('#recording-assoc-bubble button[data-click="nextTrack"]');
console.groupEnd();
if (nextButton) {
nextButton.click();
currentIndex++;
processNextTrack(processName);
} else {
console.error(`❌ Could not find the 'Next' button. Aborting.`);
cancelProcess();
}
}
function handleAutoLinkSelection() {
if (matchingMethod === 'suggested') {
const bestMatch = findBestMatchInput();
const recordingToSelect = bestMatch || document.querySelector('#recording-assoc-bubble input[data-change="recording"]');
if (recordingToSelect) {
if (bestMatch) {
console.log("🎯 Found a perfect match using cues from 'MB Release Seeding Helper'. Selecting it.");
} else {
console.log("⚠️ No perfect match found. Falling back to the first suggested recording.");
}
recordingToSelect.click();
handleConfidenceCheck('Auto-linking');
} else {
console.log("ℹ️ No suggested recordings found for this track.");
proceedToNext('Auto-linking');
}
} else if (matchingMethod === 'search') {
const searchIcon = document.querySelector('#recording-assoc-bubble img.search');
if (searchIcon) {
console.log("🕵️♂️ Searching for recordings...");
searchIcon.click();
const observer = new MutationObserver((mutations, obs) => {
const firstSearchResult = document.querySelector('.ui-autocomplete .ui-menu-item a');
const noResults = document.querySelector('.ui-autocomplete .ui-menu-item')?.textContent.includes('(No results)');
if (firstSearchResult) {
obs.disconnect();
console.log("✔️ Found search result. Selecting it.");
firstSearchResult.click();
handleConfidenceCheck('Auto-linking');
} else if (noResults) {
obs.disconnect();
console.log("❌ No search results found.");
proceedToNext('Auto-linking');
}
});
observer.observe(document.body, { childList: true, subtree: true });
} else {
console.error("❌ Could not find search icon.");
proceedToNext('Auto-linking');
}
}
}
function handleUnlinkSelection() {
console.log("🔗 Unlinking track.");
document.querySelector('#recording-assoc-bubble #add-new-recording')?.click();
proceedToNext('Unlinking');
}
function handleConfidenceCheck(processName) {
setTimeout(() => {
const confidence = getConfidenceLevel(currentTrackRow);
if (confidence && shouldIgnore(confidence)) {
console.log(`🚫 Match confidence ('${confidence}') is below threshold. Reverting to "add new recording".`);
document.querySelector('#recording-assoc-bubble #add-new-recording')?.click();
}
proceedToNext(processName);
}, 200);
}
function init() {
const recordingsTabContent = document.getElementById('recordings');
if (!recordingsTabContent) return;
const observer = new MutationObserver(() => {
const isTabActive = recordingsTabContent.getAttribute('aria-hidden') === 'false';
if (isTabActive) {
addQuickToolsButtons();
highlightAllDifferences();
} else {
removeQuickToolsButtons();
}
});
observer.observe(recordingsTabContent, { attributes: true, attributeFilter: ['aria-hidden'] });
if (recordingsTabContent.getAttribute('aria-hidden') === 'false') {
addQuickToolsButtons();
highlightAllDifferences();
}
const recordingPopup = document.getElementById('recording-assoc-bubble');
if (recordingPopup) {
const popupObserver = new MutationObserver(() => {
if (currentTrackRow) {
setTimeout(() => highlightSingleTrack(currentTrackRow), 100);
}
});
popupObserver.observe(recordingPopup, { childList: true, subtree: true });
}
document.body.addEventListener('click', (event) => {
if (event.target.classList.contains('edit-track-recording')) {
currentTrackRow = event.target.closest('tr.track');
}
}, true);
}
const bootstrapObserver = new MutationObserver((mutations, obs) => {
if (document.getElementById('recordings')) {
obs.disconnect();
init();
}
});
bootstrapObserver.observe(document.body, { childList: true, subtree: true });
})();
I’ve noticed some scripts (Quick Recording Match the latest, but there was at least one other which I don’t remember) adding changes without increasing the version number, so they don’t auto-update.
Thanks @PacCeggowk9oc, I needed the nudge - I’ve been treating my userscript github as a very loose ‘playground’, but now that I have a few scripts and a few people are using them I’ll keep a closer eye on the versioning.
I didn’t realise that auto-updates relies on higher versioning, but it’s the only thing makes sense now that I think about it!
@chaban: Keen to test out your script merger - how attached are you to being credited in the author field? Personal policy has been to not credit the ‘prompter’ for any of my scripts, so far.
The web server can send a last-modified timestamp for a requested file, that could be used for update checks as well, but then you depend on the server to be configured correctly (often with no way for the user to fix it themselves).
Userscript “xxx monkey” engines only rely on @version in the metadata block, it is not advised to try anything else.
Better use the more strict no version elements starts with letter (1.alpha should be 1.1-alpha for example) and no version elements with leading zero (2025.08.20 should be 2025.8.20), to ensure more compatibility.
I don’t care.
(20 characters)
Hey @chaban, I went to test your script improvements, but I can’t see a difference + can’t see any of the colours in your screenshots.
Did you paste the wrong version, or am I doing something stupid?
No, I just installed the pasted version on my other machine where it works too.
The colors for the entities and lengths are added by “MB Release Seeding Helper” while the highlighting for recording update toggles are added by “SUPER MIND CONTROL Ⅱ X TURBO” as part of the “TRACKLIST_TOOLS” feature.
Thanks @chaban - can you please pretend I’m an idiot and tell me what should be different when I run your version ![]()
Is there a list of MB pages to test userscripts against (with special cases, features that are rarely used…)?
Specifically I’m looking for a release with a td.video in its audio track list (video link added to an audio recording?).
There could be a nice wiki page for this, indeed.
I have a small list of big releases, which is a different need.
For my user scripts, I create the entities I need in test.musicbrainz.org before running the tests.

