// ==UserScript== // @name mb. MASS MERGE RECORDINGS // @version 2025.11.17 // @description musicbrainz.org: Merges selected or all recordings from release A to release B – List all RG recordings // @namespace https://github.com/jesus2099/konami-command // @supportURL https://community.metabrainz.org/t/merge-duplicate-recordings-between-two-editions-of-the-same-album-with-mb-mass-merge-recordings/203168?u=jesus2099 // @downloadURL https://github.com/jesus2099/konami-command/raw/master/mb_MASS-MERGE-RECORDINGS.user.js // @author jesus2099 // @licence CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/ // @licence GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @since 2011-12-13; https://web.archive.org/web/20131103163401/userscripts.org/scripts/show/120382 / https://web.archive.org/web/20141011084015/userscripts-mirror.org/scripts/show/120382 // @icon data:image/gif;base64,R0lGODlhEAAQAMIDAAAAAIAAAP8AAP///////////////////yH5BAEKAAQALAAAAAAQABAAAAMuSLrc/jA+QBUFM2iqA2ZAMAiCNpafFZAs64Fr66aqjGbtC4WkHoU+SUVCLBohCQA7 // @require https://github.com/jesus2099/konami-command/raw/198d05ea555257eaf8a2a8d8333a8cdeda28d8ad/lib/CONTROL-POMME.js?version=2024.10.25 // @require https://github.com/jesus2099/konami-command/raw/89dce29b9cce6e92e552f7d8ce2f5cb0ed161f2a/lib/MB-JUNK-SHOP.js?version=2024.10.13 // @require https://github.com/jesus2099/konami-command/raw/63aeeec359c7f1b5920308f1b105da4cce09ffe2/lib/SUPER.js?version=2025.7.21 // @grant none // @include /^https?:\/\/((beta|test)\.)?musicbrainz\.(org|eu)\/release\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(\/(disc\/\d+)?)?(\?tport=\d+)?(#.*)?$/ // @include /^https?:\/\/((beta|test)\.)?musicbrainz\.(org|eu)\/release-group\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(\?|$)/ // @run-at document-end // ==/UserScript== "use strict"; let userjs = { id: "MMR2099userjs120382", // linked to mb_INLINE-STUFF name: GM_info.script.name.substr(4).replace(/\s/g, "\u00a0"), version: GM_info.script.version }; /* - --- - --- - --- - START OF CONFIGURATION - --- - --- - --- - */ /* COLOURS */ var cOK = "greenyellow"; var cNG = "pink"; var cInfo = "gold"; var cWarning = "yellow"; var cMerge = "#fcc"; var cCancel = "#cfc"; /* - --- - --- - --- - END OF CONFIGURATION - --- - --- - --- - */ var DEBUG = localStorage.getItem("jesus2099debug"); var safeLengthDelta = 4; var largeSpread = 15; // MBS-7417 / https://github.com/metabrainz/musicbrainz-server/blob/217111e3a12b705b9499e7fdda6be93876d30fb0/lib/MusicBrainz/Server/Edit/Utils.pm#L467 var lastTick = new Date().getTime(); var MBSminimumDelay = 1000; var retryDelay = 2000; var currentButt; var MBS = location.protocol + "//" + location.host; var sidebar = document.getElementById("sidebar"); var recid2trackIndex = {remote: {}, local: {}}; // recid:tracks index var mergeQueue = []; // contains next mergeButts var sregex_MBID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; var regex_MBID = new RegExp(sregex_MBID, "i"); var css_track = "td:not(.pos):not(.video) > a[href^='/recording/'], td:not(.pos):not(.video) > :not(div):not(.ars) a[href^='/recording/']"; var css_track_ac = "td:not(.pos):not(.title):not(.rating):not(.treleases)"; var css_collapsed_medium = "div#content table.tbl.medium > thead > tr > th > a.expand-medium > span.expand-triangle"; var sregex_title = "(?:.+?[„“«‘] ?(.+) ?[“”»’] \\S+ (.+?)|(.+?)のリリース(?:グループ)?「(.+)」) - MusicBrainz"; var startpos, mergeStatus, from, to, swap, editNote, queuetrack, queueAll; var localRelease, remoteRelease; var matchMode = {current: null, sequential: null, title: null, titleAndAC: null}; var rem2loc = "◀"; var loc2rem = "▶"; var retry = {count: 0, checking: false}; if (DEBUG && GM_info && GM_info.platform) { userjs.debug = "::" + (GM_info.platform.mobile ? " mobile :: " : " ") + GM_info.platform.os + " " + GM_info.platform.browserName + " " + GM_info.platform.browserVersion.split(".")[0] + " ::"; } var css = document.createElement("style"); css.setAttribute("type", "text/css"); document.head.appendChild(css); css = css.sheet; css.insertRule("body." + userjs.id + " div#" + userjs.id + " > .main-shortcut { display: none; }", 0); css.insertRule("body." + userjs.id + " div#content table.tbl.medium > * > tr > .rating { display: none; }", 0); css.insertRule("body." + userjs.id + " div#content table.tbl.medium > tbody > tr > td > div.ars { display: none !important; }", 0); css.insertRule("body." + userjs.id + " div#content table.tbl.medium > tbody > tr > td a[href^='//acoustid.org/track/']{ display: none; }", 0); // link to Compare AcoustIDs easier! https://github.com/otringal/MB-userscripts/blob/master/Musicbrainz_acoustid.user.js css.insertRule("body:not(." + userjs.id + ") div#" + userjs.id + " { margin-top: 12px; cursor: pointer; }", 0); css.insertRule("body:not(." + userjs.id + ") div#" + userjs.id + " > :not(h2):not(.main-shortcut) { display: none; }", 0); css.insertRule("body:not(." + userjs.id + ") div#" + userjs.id + " input[name='mergeStatus'] { font-size: 9px!important; background-color: #fcf; }", 0); css.insertRule("div#" + userjs.id + " { background-color: #fcf; text-shadow: 1px 1px 2px #663; padding: 4px; margin: 0px -6px 12px; border: 2px dotted white; }", 0); css.insertRule("div#" + userjs.id + " > .main-shortcut { margin: 0px; }", 0); css.insertRule("div#" + userjs.id + " h2 { color: maroon; text-shadow: 2px 2px 4px #996; margin: 0px; }", 0); css.insertRule("div#" + userjs.id + " kbd { background-color: silver; border: 2px grey outset; padding: 0px 4px; font-size: .8em; }", 0); css.insertRule(".remoteRecordingLength.largeSpread { color: yellow; background-color: red; text-shadow: 2px 2px 4px black; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist > tbody > tr:nth-child(even) > td { background-color: #f2f2f2; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist { counter-reset: recording-index; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist > tbody > tr > td:first-child:before { counter-increment: recording-index; content: counter(recording-index); opacity: .6; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist > tbody > tr > td:first-child { text-align: center; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist > thead > tr > th:first-child { text-align: center; }", 0); css.insertRule("/*body." + userjs.id + "*/ div#content > table.tbl." + userjs.id + "reclist > tbody > tr.sameName > td:nth-child(2) { border-left: 2px solid red; }", 0); var dtitle = document.title; var ltitle = dtitle.match(new RegExp("^" + sregex_title + "$")); var release_group_MBID = location.pathname.match(new RegExp("^/release-group/(" + sregex_MBID + ")$")); var releases = document.querySelectorAll("div#content table.tbl > tbody > tr > td > a[href^='/release/'] > bdi, div#content table.tbl > tbody > tr > td > span.mp > a[href^='/release/'] > bdi"); if (ltitle) { ltitle = { artists: ltitle[2] || ltitle[3], title: ltitle[1] || ltitle[4] }; if (release_group_MBID) { if (document.getElementsByClassName("account").length > 0 && releases.length > 0) { releases = Array.prototype.slice.call(releases); sidebar.insertBefore(RGRecordingsMassMergeGUI(), sidebar.querySelector("h2.collections")); document.body.addEventListener("keydown", function(event) { if (CONTROL_POMME.ctrl_shift.test(event) && event.key.match(/^m$/i)) { loadRGRecordings(releases); return stop(event); } }); } } else { localRelease = { "release-group": document.querySelector("div.releaseheader > p.subheader a[href*='/release-group/']").getAttribute("href").match(regex_MBID)[0], title: ltitle.title, looseTitle: looseTitle(ltitle.title), comment: document.querySelector("h1 > span.comment > bdi") || "", ac: ltitle.artists, id: location.pathname.match(regex_MBID)[0], tracks: [] }; if (localRelease.comment) { localRelease.comment = "(" + localRelease.comment.textContent + ")"; } remoteRelease = {tracks: []}; if (document.getElementsByClassName("account").length > 0) { sidebar.insertBefore(massMergeGUI(), sidebar.querySelector("h2.collections")); document.body.addEventListener("keydown", function(event) { if (CONTROL_POMME.ctrl_shift.test(event) && event.key.match(/^m$/i)) { prepareLocalRelease(); return stop(event); } else if ( startpos.children.length !== 0 && matchMode.current == matchMode.sequential && CONTROL_POMME.ctrl_shift.test(event) && event.key.match(/^Arrow(Up|Down|Left|Right)$/i) ) { if (event.key.match(/^Arrow(Up|Left)$/i) && startpos.selectedIndex > 0) { startpos.selectedIndex -= 1; } else if (event.key.match(/^Arrow(Down|Right)$/i) && startpos.selectedIndex < startpos.length - 1) { startpos.selectedIndex += 1; } sendEvent(startpos, "change"); return stop(event); } }); } // sidebar.querySelector("h2.editing + ul.links").insertBefore(createTag("li", {}, [createTag("a", {}, userjs.name)]), sidebar.querySelector("h2.editing + ul.links li")); } } else { console.error("Local title (/^" + sregex_title + "$/) not found in document.title (" + document.title + ")."); } function mergeRecsStep(_step) { if (editNote.value && MBJS.isValidEditNote(editNote.value)) { editNote.style.removeProperty("background-color"); if (editNote.nextSibling.matches("p.error." + userjs.id)) { editNote.parentNode.removeChild(editNote.nextSibling); } var step = _step || 0; var MMR = document.getElementById(userjs.id); var statuses = ["adding recs. to merge", "applying merge edit"]; var buttStatuses = ["Stacking…", "Merging…"]; var urls = ["/recording/merge_queue", "/recording/merge"]; var params = [ "add-to-merge=" + to.value + "&add-to-merge=" + from.value, "merge.merging.0=" + to.value + "&merge.target=" + to.value + "&merge.merging.1=" + from.value ]; disableInputs([matchMode.sequential, matchMode.title, matchMode.titleAndAC, startpos, mergeStatus]); if (step == 1) { disableInputs([editNote, currentButt, currentButt.parentNode.querySelector("input." + userjs.id + "dirbutt")]); params[step] += "&merge.edit_note="; var paramsup = MMR.getElementsByTagName("textarea")[0].value.trim(); if (paramsup != "") paramsup += "\n —\n"; paramsup += releaseInfoRow("source", swap.value == "no" ? remoteRelease : localRelease, swap.value == "no" ? recid2trackIndex.remote[from.value] : recid2trackIndex.local[from.value]); paramsup += releaseInfoRow("target", swap.value == "no" ? localRelease : remoteRelease, swap.value == "no" ? recid2trackIndex.local[to.value] : recid2trackIndex.remote[to.value]); paramsup += " —\n"; var targetID = parseInt(to.value, 10); var sourceID = parseInt(from.value, 10); if (sourceID > targetID) { paramsup += "👍 '''Targeting oldest [MBID]''' (" + format(to.value) + " ← " + format(from.value) + ")" + "\n"; } var locTrack = localRelease.tracks[recid2trackIndex.local[swap.value == "no" ? to.value : from.value]]; var remTrack = remoteRelease.tracks[recid2trackIndex.remote[swap.value == "no" ? from.value : to.value]]; if (locTrack.name == remTrack.name) paramsup += "👍 '''Same track title''' “" + protectEditNoteText(locTrack.name) + "”\n"; else if (locTrack.name.toUpperCase() == remTrack.name.toUpperCase()) paramsup += "👍 '''Same track title''' (case insensitive)\n"; else if (locTrack.looseName == remTrack.looseName) paramsup += "👍 '''Similar track title''' (loose comparison)\n"; if (locTrack.artistCredit == remTrack.artistCreditAsPlainText) paramsup += "👍 '''Same track artist credit ([AC])''' “" + locTrack.artistCredit + "”\n"; else if (locTrack.artistCredit.toUpperCase() == remTrack.artistCreditAsPlainText.toUpperCase()) paramsup += "👍 '''Same track artist credit ([AC])''' (case insensitive)\n"; else if (locTrack.looseAC == remTrack.looseAC) paramsup += "👍 '''Similar track artist credit ([AC])''' “" + locTrack.artistCredit + "”\n"; if (typeof locTrack.length == "number" && typeof remTrack.length == "number") { var delta = Math.abs(locTrack.length - remTrack.length); if (delta <= safeLengthDelta * 1000) paramsup += "👍 '''" + (delta === 0 ? "Same" : "Very close") + " track times''' " + /* temporary hidden until milliseconds are back (delta === 0 ? "(in milliseconds)" : */ "(" + (time(locTrack.length) == time(remTrack.length) ? time(locTrack.length) : "within " + safeLengthDelta + " seconds: " + time((swap.value == "no" ? locTrack : remTrack).length) + " ← " + time((swap.value == "no" ? remTrack : locTrack).length)) + ")" /* ) temporary */ + "\n"; } if (localRelease.ac == remoteRelease.ac) paramsup += "👍 '''Same release artist''' “" + protectEditNoteText(localRelease.ac) + "”\n"; if (localRelease.title == remoteRelease.title) paramsup += "👍 '''Same release title''' “" + protectEditNoteText(localRelease.title) + "”\n"; else if (localRelease.title.toUpperCase() == remoteRelease.title.toUpperCase()) paramsup += "👍 '''Same release title''' (case insensitive)\n"; else if (localRelease.looseTitle == remoteRelease.looseTitle) paramsup += "👍 '''Almost same release title''' (loose comparison)\n"; // else if (leven(localRelease.looseTitle, remoteRelease.looseTitle)) paramsup += "👍 '''Almost same release title''' (loose comparison)\n"; if (localRelease["release-group"] == remoteRelease["release-group"]) paramsup += "👍 '''Same release group''' (" + MBS + "/release-group/" + localRelease["release-group"] + ")\n"; paramsup += " —\n" + userjs.name + " (" + userjs.version + ") in “" + matchMode.current.value.replace(/^Match unordered /i, "") + "” match mode"; if (retry.count > 0) { paramsup += " — '''retry'''" + (retry.count > 1 ? " #" + retry.count : "") + " (" + protectEditNoteText(retry.message) + ")"; } if (userjs.debug) { paramsup += " " + userjs.debug; } params[step] += encodeURIComponent(paramsup); } infoMerge("#" + from.value + " to #" + to.value + " " + statuses[step] + "…"); currentButt.setAttribute("value", buttStatuses[step] + " " + (step + 1) + "/2"); currentButt.setAttribute("ref", step); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(event) { if (this.readyState == 4) { if (to.value === "") { nextButt(false); } else if (this.status == 200) { var response = document.createElement("html"); response.innerHTML = this.responseText; if (step === 0) { if ( response.querySelector("form[method='post'] table.tbl input[type='radio'][name='merge.target'][value='" + from.value + "']") && response.querySelector("form[method='post'] table.tbl a[href$='/recording/" + from.getAttribute("ref") + "']") && response.querySelector("form[method='post'] table.tbl input[type='radio'][name='merge.target'][value='" + to.value + "']") && response.querySelector("form[method='post'] table.tbl a[href$='/recording/" + to.getAttribute("ref") + "']") ) { mergeRecsStep(1); } else { tryAgain("Did not queue"); } } else if (step === 1) { if ( response.querySelector("h1 > span.mp > a[href$='/recording/" + to.getAttribute("ref") + "']") && response.querySelector("a[href*='/recording/merge_queue?add-to-merge=" + to.value + "']") ) { nextButt(true); } else { checkMerge("Did not merge"); } } } else { var errorText = "Error " + this.status + " “" + this.statusText + "” in step " + (step + 1) + "/2"; if (step === 0) { tryAgain(errorText); } else { checkMerge(errorText); } } } }; xhr.open("POST", MBS + urls[step], true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); setTimeout(function() { xhr.send(params[step]); }, chrono(MBSminimumDelay)); } else { if (!editNote.nextSibling.matches("p.error." + userjs.id)) { editNote.parentNode.insertBefore(createTag("p", {a: {class: "error"}}, "Merging recordings is a destructive edit that is impossible to undo without losing ISRCs, AcoustIDs, edit histories, etc.\n\nPlease make sure your edit note makes it clear why you are sure that these recordings are exactly the same versions, mixes, cuts, etc."), editNote); addAfter(createTag("p", {a: {class: "error " + userjs.id}}, MBJS.getText("invalid_edit_note")), editNote); } editNote.style.setProperty("background-color", cNG); infoMerge("Invalid edit note.", false, true); } } function releaseInfoRow(sourceOrTarget, rel, trackIndex) { var spaced_comment = rel.comment ? " " + rel.comment : ""; return sourceOrTarget + ": " + MBS + "/release/" + rel.id + " #'''" + (trackIndex + 1) + "'''/" + rel.tracks.length + ". “'''" + protectEditNoteText(rel.title) + "'''”" + protectEditNoteText(spaced_comment) + " by '''" + protectEditNoteText(rel.ac) + "'''\n"; } function checkMerge(errorText) { retry.checking = true; infoMerge("Checking merge (" + errorText + ")…", false); var xhr = new XMLHttpRequest(); xhr.addEventListener("error", function(event) { setTimeout(function() { infoMerge("Retrying in 2s (error " + this. status + ": “" + this.statusText + "”)…", false); checkMerge(errorText); }, 2000); }); xhr.addEventListener("load", function(event) { if (this.status < 200 || this.status >= 400) { sendEvent(this, "error"); } else { if (typeof this.responseText == "string") { if (this.responseText.indexOf('class="edit-list"') > -1) { var editID = this.responseText.match(/>Edit #(\d+)/); nextButt(editID ? editID[1] : true); } else if (this.responseText.indexOf('id="remove.' + from.value + '"') > -1 && this.responseText.indexOf('id="remove.' + to.value + '"') > -1) { retry.count += 1; retry.message = errorText; mergeRecsStep(1); } else { tryAgain(errorText); } } else { sendEvent(this, "error"); } retry.checking = false; } }); xhr.open("GET", MBS + "/search/edits?negation=0&combinator=and&conditions.0.field=recording&conditions.0.operator=%3D&conditions.0.name=" + from.value + "&conditions.0.args.0=" + from.value + "&conditions.1.field=recording&conditions.1.operator=%3D&conditions.1.name=" + to.value + "&conditions.1.args.0=" + to.value + "&conditions.2.field=type&conditions.2.operator=%3D&conditions.2.args=74&conditions.3.field=status&conditions.3.operator=%3D&conditions.3.args=1", true); setTimeout(function() { xhr.send(null); }, chrono(retryDelay)); } function nextButt(successOrEditID) { if (successOrEditID !== false) { remoteRelease.tracks[recid2trackIndex.remote[swap.value == "no" ? from.value : to.value]].recording.editsPending++; cleanTrack(localRelease.tracks[recid2trackIndex.local[swap.value == "no" ? to.value : from.value]], successOrEditID || true, retry.count); infoMerge("#" + from.value + " to #" + to.value + " merged OK", true, true); } else { infoMerge("Merge cancelled", true, true); currentButt.value = "Merge"; enableInputs(currentButt); } retry.count = 0; currentButt = null; document.title = dtitle; updateMatchModeDisplay(); enableInputs([mergeStatus, editNote]); var nextButtFromQueue = mergeQueue.shift(); if (nextButtFromQueue) { enableAndClick(nextButtFromQueue); } } function tryAgain(errorText) { retry.count += 1; retry.message = errorText; var errormsg = errorText; if (currentButt) { errormsg = "Retry in " + Math.ceil(retryDelay / 1000) + " seconds (" + errormsg + ")."; setTimeout(function() { enableAndClick(currentButt); }, retryDelay); } infoMerge(errormsg, false, true); } function enableAndClick(butt) { enableInputs(butt); butt.value = "Merge"; butt.click(); } function infoMerge(msg, goodNews, reset) { mergeStatus.value = msg; if (goodNews != null) { mergeStatus.style.setProperty("background-color", goodNews ? cOK : cNG); } else { mergeStatus.style.setProperty("background-color", cInfo); } if (reset) { from.value = ""; to.value = ""; } } function queueTrack() { queuetrack.replaceChild(document.createTextNode(mergeQueue.length + " queued merge" + (mergeQueue.length > 1 ? "s" : "")), queuetrack.firstChild); queuetrack.style.setProperty("display", mergeQueue.length > 0 ? "block" : "none"); document.title = (mergeQueue.length + 1) + "⌛ " + dtitle; } function cleanTrack(track, editID, retryCount) { var rmForm = track.tr.querySelector("td:not(.pos):not(.video) form." + userjs.id); if (rmForm) { if (editID) { mp(track.tr.querySelector(css_track), true); var noPendingOpenEdits = document.querySelector("div#sidebar :not(.mp) > a[href='/release/" + localRelease.id + "/open_edits']"); var mb_PENDING_EDITS = document.querySelectorAll("div#sidebar .jesus2099PendingEditsCount"); for (let counts = 0; counts < mb_PENDING_EDITS.length; counts++) { var currentCount = mb_PENDING_EDITS[counts].textContent.trim(); if ((currentCount = currentCount.match(/^\d+$/)) && mb_PENDING_EDITS[counts].style.getPropertyValue("background-color") != "pink") { mb_PENDING_EDITS[counts].replaceChild(document.createTextNode(parseInt(currentCount, 10) + 1), mb_PENDING_EDITS[counts].firstChild); } } if (noPendingOpenEdits) { if (mb_PENDING_EDITS.length > 0) { noPendingOpenEdits.parentNode.classList.add("mp"); noPendingOpenEdits.style.removeProperty("text-decoration"); for (let counts = 0; counts < mb_PENDING_EDITS.length; counts++) { mb_PENDING_EDITS[counts].parentNode.parentNode.removeAttribute("title"); mb_PENDING_EDITS[counts].parentNode.parentNode.style.removeProperty("opacity"); } } else { mp(noPendingOpenEdits, true); } } removeChildren(rmForm); if (typeof editID == "number" || typeof retryCount == "number" && retryCount > 0) { var infoSpan = addAfter(createTag("span", {s: {opacity: ".5"}}, [" (", createTag("span"), ")"]), rmForm).querySelector("span > span"); if (typeof editID == "number") { infoSpan.appendChild(createTag("span", {a: {class: "mp"}}, createA("edit:" + editID, "/edit/" + editID))); } if (typeof retryCount == "number" && retryCount > 0) { if (infoSpan.childNodes.length > 0) { infoSpan.appendChild(document.createTextNode(", ")); } var retryLabel = "retr"; if (retryCount > 1) { retryLabel = retryCount + " " + retryLabel + "ies"; } else { retryLabel += "y"; } infoSpan.appendChild(createA(retryLabel, track.a.getAttribute("href") + "/edits")); } } } else { removeNode(rmForm); } } else { var lengthcell = track.tr.querySelector("td.treleases"); if (track.length && lengthcell) { lengthcell.replaceChild(document.createTextNode(time(track.length, false)), lengthcell.firstChild); lengthcell.style.setProperty("font-family", "monospace"); lengthcell.style.setProperty("text-align", "right"); } } } function changeMatchMode(event) { matchMode.current = event.target; updateMatchModeDisplay(); if (matchMode.current == matchMode.sequential) { spreadTracks(event); } else { var matchedRemoteTracks = []; for (let loc = 0; loc < localRelease.tracks.length; loc++) { cleanTrack(localRelease.tracks[loc]); var rem = bestStartPosition(loc, matchMode.current == matchMode.titleAndAC); if (rem !== null) { rem = 0 - rem + loc; if (matchedRemoteTracks.indexOf(rem) < 0) { matchedRemoteTracks.push(rem); buildMergeForm(loc, rem); } } } var notMatched = remoteRelease.tracks.length - matchedRemoteTracks.length; infoMerge((notMatched === 0 ? "All" : "☞") + " " + matchedRemoteTracks.length + " remote track title" + (matchedRemoteTracks.length == 1 ? "" : "s") + " matched" + (notMatched > 0 ? " (" + notMatched + " left)" : ""), matchedRemoteTracks.length > 0); } } function updateMatchModeDisplay() { for (let mode in matchMode) if (Object.prototype.hasOwnProperty.call(matchMode, mode)) { disableInputs(matchMode[mode], matchMode[mode] == matchMode.current); } enableInputs(startpos, matchMode.sequential == matchMode.current); } function massMergeGUI() { var MMRdiv = createTag("div", {a: {id: userjs.id}, e: { keydown: function(event) { if (event.target == editNote && CONTROL_POMME.ctrl.test(event)) { switch (event.key) { case "s": return saveEditNote(event); case "o": return loadEditNote(event); case "Enter": queueAll.click(); } } }, click: prepareLocalRelease }}, [ createTag("h2", {}, userjs.name), createTag("p", {}, "version " + userjs.version), createTag("p", {a: {"class": "main-shortcut"}}, ["☞ ", CONTROL_POMME.ctrl_shift.label, "M"]), createTag("p", {s: {marginBottom: "0px!"}}, ["Remote release: ", createTag("span", {a: {"class": "remote-release-link"}})]), ]); mergeStatus = MMRdiv.appendChild(createInput("text", "mergeStatus", "", userjs.name + " remote release URL")); mergeStatus.style.setProperty("width", "100%"); mergeStatus.addEventListener("input", function(event) { matchMode.current = matchMode.sequential; updateMatchModeDisplay(); var mbid = this.value.match(new RegExp("/release/(" + sregex_MBID + ")(/disc/(\\d+))?")); if (mbid) { localRelease.tracks = []; recid2trackIndex.local = {}; removeChildren(startpos); var trs = document.querySelectorAll("div#content table.tbl.medium > tbody > tr"); /* var jsonRelease, scripts = document.querySelectorAll("script:not([src])"); for (let s = 0; s < scripts.length && !jsonRelease; s++) { jsonRelease = scripts[s].textContent.match(/MB\.Release\.init\(([^<]+)\)/); } if (jsonRelease) jsonRelease = JSON.parse(jsonRelease[1]); */ var multiDiscRelease = document.querySelectorAll(css_collapsed_medium).length > 1; for (let itrs = 0, t = 0, d = 0, dt = 0; itrs < trs.length; itrs++) { if (!trs[itrs].classList.contains("subh")) { var tracka = trs[itrs].querySelector(css_track); var recoid = trs[itrs].querySelector("td.rating a.set-rating").getAttribute("href").match(/id=([0-9]+)/)[1]; var trackname = tracka.textContent; var trackLength = trs[itrs].querySelector("td.treleases").textContent.match(/(\d+:)?\d+:\d+/); if (trackLength) trackLength = strtime2ms(trackLength[0]); var trackAC = trs[itrs].querySelector(css_track_ac); localRelease.tracks.push({ tr: trs[itrs], disc: d, track: dt, a: tracka, recid: recoid, name: trackname, artistCredit: trackAC ? trackAC.textContent.trim() : localRelease.ac, length: trackLength }); localRelease.tracks[t].looseName = looseTitle(localRelease.tracks[t].name); localRelease.tracks[t].looseAC = looseTitle(localRelease.tracks[t].artistCredit); /* if (jsonRelease) { // localRelease.tracks[localRelease.tracks.length - 1] = jsonRelease.mediums[d - 1].tracks[dt]; for (let key in jsonRelease.mediums[d - 1].tracks[dt]) if (jsonRelease.mediums[d - 1].tracks[dt].hasOwnProperty(key)) { localRelease.tracks[localRelease.tracks.length - 1][key] = jsonRelease.mediums[d - 1].tracks[dt][key]; } } */ dt++; recid2trackIndex.local[recoid] = t; addOption(startpos, t, (multiDiscRelease ? d + "." : "") + dt + ". " + trackname); t++; } else if (!trs[itrs].querySelector("div.data-track")) { d++; dt = 0; } } this.setAttribute("ref", this.value); remoteRelease.id = mbid[1]; remoteRelease.disc = mbid[2] || ""; infoMerge("Fetching recordings…"); loadReleasePage(); // loadReleaseWS(remoteRelease.id); } }); MMRdiv.appendChild(createTag("p", {}, "Once you paste the remote release URL, all its recordings will be loaded and made available for merge with the local recordings in the left hand tracklist.")); MMRdiv.appendChild(createTag("p", {}, "Herebelow, you can shift the alignment of local and remote tracklists.")); MMRdiv.appendChild(createTag("p", {s: {marginBottom: "0px"}}, "Remote tracklist start position:")); /* track parsing */ startpos = MMRdiv.appendChild(createTag("select", {s: {fontSize: ".8em", width: "100%"}, e: {change: function(event) { /* hitting ENTER on a once changed