// ==UserScript==
// @name mb. PENDING EDITS
// @version 2024.2.15
// @description musicbrainz.org: Adds/fixes links to entity (pending) edits (if any); optionally adds links to associated artist(s) (pending) edits
// @namespace https://github.com/jesus2099/konami-command
// @supportURL https://github.com/jesus2099/konami-command/labels/mb_PENDING-EDITS
// @downloadURL https://github.com/jesus2099/konami-command/raw/master/mb_PENDING-EDITS.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 2009-02-09; https://web.archive.org/web/20140328150654/userscripts.org/scripts/show/42102 / https://web.archive.org/web/20141011084020/userscripts-mirror.org/scripts/show/42102
// @icon data:image/gif;base64,R0lGODlhEAAQAMIDAAAAAIAAAP8AAP///////////////////yH5BAEKAAQALAAAAAAQABAAAAMuSLrc/jA+QBUFM2iqA2ZAMAiCNpafFZAs64Fr66aqjGbtC4WkHoU+SUVCLBohCQA7
// @require https://github.com/jesus2099/konami-command/raw/de88f870c0e6c633e02f32695e32c4f50329fc3e/lib/SUPER.js?version=2022.3.24.224
// @grant none
// @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/[^/]+\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
// @run-at document-end
// ==/UserScript==
"use strict";
// “const” NG in Opera 12 at least
var SCRIPT_KEY = "jesus2099PendingEdits"; // linked in mb_MASS-MERGE-RECORDINGS.user.js
var MBS = self.location.protocol + "//" + self.location.host;
var RE_GUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
var pageEntity, checked = [], xhrPendingEdits = {};
var account = document.querySelector("div.header li.account a[href^='/user/']");
// EDITING HISTORY
if (
account
&& (account = decodeURIComponent(account.getAttribute("href").match(/[^/]+$/)))
&& document.querySelector("div#sidebar")
&& (pageEntity = document.querySelector("div#content > div > h1 a"))
&& (pageEntity = a2obj(pageEntity))
&& (pageEntity.type = self.location.pathname.match(new RegExp("^/([^/]+)/(" + RE_GUID + ")")))
&& (pageEntity.type = pageEntity.type[1].replace("-", "_"))
) {
pageEntity.editinghistory = document.querySelector("div#sidebar ul.links a[href$='" + pageEntity.base + "/edits']");
if (pageEntity.editinghistory) {
pageEntity.ul = getParent(pageEntity.editinghistory, "ul");
} else {
pageEntity.ul = document.querySelector("div#sidebar ul.links");
pageEntity.editinghistory = createLink("edits"); // reverts MBS-57 (Remove “normal artist” functionality from Various Artists) drawback
}
if (!/^collection$/.test(pageEntity.type)) {
// TODO: Allow collections with missing MBS-3922 feature “Edit search: Filter edits by collections” https://tickets.metabrainz.org/browse/MBS-3922
appendRefineSearchFormLink(pageEntity.editinghistory);
}
pageEntity.li = getParent(pageEntity.editinghistory, "li");
// OPEN EDITS
pageEntity.openedits = document.querySelector("div#sidebar a[href$='" + pageEntity.base + "/open_edits']");
if (pageEntity.openedits) {
// pageEntity.openedits.removeAttribute("title"); // removes bogus tooltip (artist disambiguation or swapped sort name) that is masking our useful tooltip
if (pageEntity.openedits.parentNode.tagName == "LI") { // fixes MBS-2298 (“Open edits” link should share same styling as pending edit items)
var pendingEditsMarkedLink = createTag("span", {a: {class: "mp"}});
pageEntity.openedits.parentNode.replaceChild(pendingEditsMarkedLink.appendChild(pageEntity.openedits.cloneNode(true)).parentNode, pageEntity.openedits);
pageEntity.openedits = pendingEditsMarkedLink.firstChild; // restore node parental context
}
} else {
pageEntity.openedits = createLink("open_edits"); // fixes MBS-3386 (“Open edits” link not always displayed)
}
checked.push(pageEntity.base);
checkOpenEdits(pageEntity);
// ASSOCIATED ARTIST LINKS
if (!/^(area|artist|collection|label)$/.test(pageEntity.type)) {
var artists;
switch (pageEntity.type) {
case "release_group":
case "release":
case "recording":
artists = document.querySelectorAll("p.subheader a[href^='/artist/']");
break;
case "url":
artists = document.querySelectorAll("div#content a[href^='/artist/'], div#content a[href^='/label/']");
break;
case "work":
artists = workMainArtists();
break;
}
if (artists && artists.length && artists.length > 0) {
for (var arti = artists.length - 1; arti >= 0; arti--) {
var art = a2obj(artists[arti]);
if (checked.indexOf(art.base) < 0) {
checked.push(art.base);
art.editinghistory = createLink("edits", art);
art.openedits = createLink("open_edits", art);
getParent(art.openedits, "li").classList.add("separator");
checkOpenEdits(art);
}
}
}
}
}
function createLink(historyType, associatedArtist) {
var currentEntity = associatedArtist || pageEntity;
var linkLabel = (historyType == "edits" ? "editing\u00a0history" : "open\u00a0edits");
linkLabel = associatedArtist ? currentEntity.name + " " + linkLabel : linkLabel.replace(/(.)(.*)/, function(match, g1, g2 /* , offset, string */) { return g1.toUpperCase() + g2; });
var newLink = createTag("li", null, createTag("span", null, createTag("a", {a: {href: currentEntity.base + "/" + historyType}}, linkLabel))); // “span.(""|"mp")” linked in mb_MASS-MERGE-RECORDINGS.user.js
if (associatedArtist) {
addAfter(newLink, pageEntity.li);
} else if (!associatedArtist && historyType == "edits") {
newLink.classList.add("separator");
pageEntity.ul.appendChild(newLink);
} else {
pageEntity.ul.insertBefore(newLink, pageEntity.li);
}
return newLink.firstChild.firstChild;
}
function appendRefineSearchFormLink(baseEditLink) {
// Use Merge link to find entity row ID
pageEntity.id = document.querySelector("div#sidebar a[href^='/rating/rate/?entity_type=" + pageEntity.type + "&entity_id='], div#sidebar a[href*='/merge_queue?add-to-merge='], div#sidebar a[href^='/collection/create?']");
if (pageEntity.id) {
pageEntity.id = pageEntity.id.getAttribute("href").match(/=(\d+)/);
if (pageEntity.id) {
pageEntity.id = pageEntity.id[1];
addAfter(createTag("span", {}, [" (", createTag("a", {s: {backgroundColor: "#FF6"}, a: {title: "Exclude failed edits\n(this link is added by mb. PENDING EDITS)", href: "/search/edits?order=desc&negation=0&combinator=and&conditions.0.field=" + pageEntity.type + "&conditions.0.operator=%3D&conditions.0.name=" + encodeURIComponent(pageEntity.name) + "&conditions.0.args.0=" + pageEntity.id + "&conditions.1.field=status&conditions.1.operator=%3D&conditions.1.args=1&conditions.1.args=2"}}, "effective"), ")"]), baseEditLink);
}
}
}
function checkOpenEdits(obj) {
var smp = getParent(obj.openedits, "li").firstChild;
var count = smp.querySelector("span." + SCRIPT_KEY + "Count");
if (!count) {
smp.appendChild(document.createTextNode("\u00a0("));
smp.appendChild(createTag("span", {a: {class: SCRIPT_KEY + "Count"}}, createTag("img", {a: {alt: "⌛ loading…", src: "/static/images/icons/loading.gif", height: self.getComputedStyle(smp).getPropertyValue("font-size")}}))); // “SCRIPT_KEY + "Count"” linked in mb_MASS-MERGE-RECORDINGS.user.js
smp.appendChild(document.createTextNode(")"));
}
xhrPendingEdits[obj.base] = {
object: obj,
xhr: new XMLHttpRequest()
};
xhrPendingEdits[obj.base].xhr.addEventListener("load", function() {
var xhrpe;
for (var entityBasePath in xhrPendingEdits) if (Object.prototype.hasOwnProperty.call(xhrPendingEdits, entityBasePath) && xhrPendingEdits[entityBasePath].xhr == this) {
xhrpe = xhrPendingEdits[entityBasePath];
break;
}
if (this.status == 200) {
var responseDOM = document.createElement("html"); responseDOM.innerHTML = this.responseText;
var editCount = responseDOM.querySelector("div.search-toggle");
var editDetails = {types: [], editors: [], editCount: 0, paginated: false, atLeast: false};
if (
editCount
&& (editCount = editCount.textContent.match(/\d+/))
&& (editCount = parseInt(editCount[0], 10))
&& !isNaN(editCount)
) {
var pagination = responseDOM.querySelector("ul.pagination");
editDetails = {
types: Array.from(this.responseText.match(/<h2><a href="[^"]+"><bdi>[^<]+<\/bdi><\/a><\/h2>/g), x => x.replace(/^.+ [-–] /, "- ").replace(/<\/bdi>.+$/, "")),
editors: Array.from(this.responseText.match(/<\/h2><p class="subheader">[\S\s]+?<a href="\/user\/[^/]+">[\S\s]+?<\/p>/g), x => decodeURIComponent(x.replace(/^[\S\s]+\/user\/|">[\S\s]+$/g, ""))),
editCount: editCount
};
if (pagination) {
var pages = pagination.getElementsByTagName("li");
if (
pages[pages.length - 1].querySelector("a[href$='" + xhrpe.object.base + "/open_edits?page=2']")
&& pages[pages.length - 2].classList.contains("separator")
&& pages[pages.length - 3].textContent.trim() == "…"
) {
editDetails.atLeast = true;
}
editDetails.paginated = true;
}
}
if (editDetails.editCount == 0 || editDetails.types.length == editDetails.editors.length) {
updateLink(xhrpe.object, editDetails);
} else {
updateLink(xhrpe.object, "type and editor counts mismatch");
}
} else {
updateLink(xhrpe.object, this.status + ": " + this.statusText);
}
});
xhrPendingEdits[obj.base].xhr.open("get", MBS + obj.openedits.getAttribute("href"), true);
xhrPendingEdits[obj.base].xhr.setRequestHeader("base", obj.base);
xhrPendingEdits[obj.base].xhr.send(null);
}
function updateLink(obj, details) {
var countText;
var li = getParent(obj.openedits, "li");
var count = li.querySelector("span." + SCRIPT_KEY + "Count");
if (typeof details == "object") {
countText = details.editCount;
if (details.editCount == 0) {
mp(obj.openedits, false);
} else if (details.editCount > 0) {
mp(obj.openedits, true);
if (details.atLeast) countText += "+";
var titarray = [], dupcount = 0, dupreset;
for (var d = 0; d < details.types.length; d++) {
var thistit = details.types[d];
var editor = details.editors[d];
if (editor != account) {
thistit += " (" + editor + ")";
}
if (thistit != titarray[titarray.length - 1]) {
titarray.push(thistit);
if (d > 0) {
dupreset = true;
}
} else {
dupcount++;
}
var last = (d == details.types.length - 1);
if (dupcount > 0 && (dupreset || last)) {
titarray[titarray.length - 2 + (!dupreset && last ? 1 : 0)] += " ×" + (dupcount + 1);
dupcount = 0;
}
dupreset = false;
}
var expanded = "▼";
var collapsed = "◀";
var expandEditLists = (localStorage.getItem(SCRIPT_KEY + "PendingEditLists") != collapsed);
var ul = createTag("ul", {a: {class: SCRIPT_KEY + "EditList"}, s: {display: expandEditLists ? "block" : "none", opacity: ".8"}});
for (var e = 0; e < titarray.length; e++) {
var edit1type2editor3count = titarray[e].match(/^(?:- )?([^(]+)(?: \(([^)]+)\))?(?: ×(\d+))?$/);
var editLi = ul.appendChild(createTag("li", {}, createTag("span", {a: {class: "mp"}}, edit1type2editor3count[1] + (edit1type2editor3count[3] ? " ×" + edit1type2editor3count[3] : ""))));
if (edit1type2editor3count[2]) {
editLi.appendChild(document.createTextNode(" by "));
editLi.appendChild(createTag("a", {a: {href: "/user/" + escape(edit1type2editor3count[2])}}, edit1type2editor3count[2]));
} else {
editLi.style.setProperty("font-weight", "bold");
}
}
if (details.paginated) {
ul.appendChild(createTag("li", {}, ["And ", createTag("span", {a: {class: "mp"}}, createTag("a", {a: {href: obj.openedits.getAttribute("href") + "?page=2"}}, (details.editCount - details.types.length) + " older edit" + (details.editCount == (details.types.length + 1) ? "" : "s"))), "…"]));
}
var help = createTag("span", {a: {class: SCRIPT_KEY + "Help"}, s: {display: expandEditLists ? "inline" : "none"}});
if (titarray.length > 1) {
help.appendChild(document.createElement("br"));
help.appendChild(document.createTextNode(" newest edit on top"));
}
li.appendChild(help);
li.appendChild(ul);
li.insertBefore(createTag("a", {a: {class: SCRIPT_KEY + "Toggle"}, s: {position: "absolute", right: "4px"}, e: {click: function(event) {
var collapse = (this.textContent == expanded);
for (var options = document.querySelectorAll("ul." + SCRIPT_KEY + "EditList, span." + SCRIPT_KEY + "Help"), o = 0; o < options.length; o++) {
options[o].style.setProperty("display", collapse ? "none" : (options[o].tagName == "UL" ? "block" : "inline"));
}
for (var toggles = document.querySelectorAll("a." + SCRIPT_KEY + "Toggle"), t = 0; t < toggles.length; t++) {
replaceChildren(document.createTextNode(collapse ? collapsed : expanded), toggles[t]);
}
localStorage.setItem(SCRIPT_KEY + "PendingEditLists", collapse ? collapsed : expanded);
}}}, expandEditLists ? expanded : collapsed), li.firstChild);
}
} else {
count.style.setProperty("background-color", "pink"); // “pink” linked in mb_MASS-MERGE-RECORDINGS.user.js
countText = details;
}
count.replaceChild(document.createTextNode(countText), count.firstChild); // “countText” linked in mb_MASS-MERGE-RECORDINGS.user.js
}
function mp(o, set) {
var li = getParent(o, "li");
if (typeof set == "undefined") {
return li.firstChild.tagName == "SPAN" && li.firstChild.classList.contains("mp");
} else if (typeof set == "boolean" && li.firstChild.tagName == "SPAN") {
if (set && !mp(o)) {
li.firstChild.className = "mp";
} else if (!set) {
if (mp(o)) {
li.firstChild.classList.remove("mp");
}
o.style.setProperty("text-decoration", "line-through"); // linked in mb_MASS-MERGE-RECORDINGS.user.js
li.style.setProperty("opacity", ".5"); // linked in mb_MASS-MERGE-RECORDINGS.user.js
}
}
}
function a2obj(a) {
return {
a: a,
name: a.textContent,
base: a.getAttribute("href").match(new RegExp("(/[^/]+/" + RE_GUID + ")$"))[1]
};
}
function workMainArtists() {
var writers = document.querySelectorAll("div#content > table.details > tbody td a[href^='/artist/']");
var groupedWriters = {};
for (let w = 0; w < writers.length; w++) {
let href = writers[w].getAttribute("href");
if (!groupedWriters[href]) {
groupedWriters[href] = [];
}
groupedWriters[href].push(writers[w]);
}
var performers = document.querySelectorAll("div#content > table.tbl > tbody td a[href^='/artist/']");
var groupedPerformers = {};
for (let p = 0; p < performers.length; p++) {
let href = performers[p].getAttribute("href");
if (!groupedPerformers[href]) {
groupedPerformers[href] = [];
}
groupedPerformers[href].push(performers[p]);
}
var mainArtists = [];
var max = 0;
// find most frequent performer(s)
for (let href in groupedPerformers) if (Object.prototype.hasOwnProperty.call(groupedPerformers, href)) {
if (groupedPerformers[href].length > max) {
max = groupedPerformers[href].length;
mainArtists = [groupedPerformers[href][0]];
} else if (groupedPerformers[href].length == max) {
// take first ex‐æquo artist(s) too
mainArtists.push(groupedPerformers[href][0]);
}
}
// take all singer/song‐writers; take all writers if no performers
for (let href in groupedWriters) if (Object.prototype.hasOwnProperty.call(groupedWriters, href) && (groupedPerformers[href] || performers.length < 1)) {
mainArtists.push(groupedWriters[href][0]);
}
// get artist main names whenever possible
for (let f = 0; f < mainArtists.length; f++) {
let noNameVariationArtist = document.querySelector(":not(.name-variation) > a[href='" + mainArtists[f].getAttribute("href") + "']");
mainArtists[f] = noNameVariationArtist || mainArtists[f];
}
return mainArtists;
}
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.