NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name mb. HYPER MOULINETTE // @version 2024.3.23 // @description musicbrainz.org: Mass PUT or DELETE releases in a collection from an edit search, an other collection, or a tag // @namespace https://github.com/jesus2099/konami-command // @supportURL https://github.com/jesus2099/konami-command/labels/mb_HYPER-MOULINETTE // @downloadURL https://github.com/jesus2099/konami-command/raw/master/mb_HYPER-MOULINETTE.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 2014-09-19 // @icon data:image/gif;base64,R0lGODlhEAAQAKEDAP+/3/9/vwAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh/glqZXN1czIwOTkAIfkEAQACAwAsAAAAABAAEAAAAkCcL5nHlgFiWE3AiMFkNnvBed42CCJgmlsnplhyonIEZ8ElQY8U66X+oZF2ogkIYcFpKI6b4uls3pyKqfGJzRYAACH5BAEIAAMALAgABQAFAAMAAAIFhI8ioAUAIfkEAQgAAwAsCAAGAAUAAgAAAgSEDHgFADs= // @require https://github.com/jesus2099/konami-command/raw/de88f870c0e6c633e02f32695e32c4f50329fc3e/lib/SUPER.js?version=2022.3.24.224 // @grant none // @match *://*.musicbrainz.org/user/*/collections // @run-at document-end // ==/UserScript== "use strict"; let userjs = { id: "jesus2099", name: GM_info.script.name.substr(4) }; var DEBUG = false; userjs.id += userjs.name.replace(/ /, "-"); var stre_GUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; var re_GUID = new RegExp(stre_GUID); var account = document.querySelector("ul.menu li.account"); var target, method, source, client, loaders = []; var crawlType = { "^/collection/": "div#page table.tbl tbody a[href*='/release/']", "^/search/edits": "div.edit-details a[href*='/release/']", "/tag/[^/]+/release\\?page=\\d+$": "a[href^='/release/']", }; var genuineTitle = document.title; // ========================================================================== // ## MENU ITEM ## // find this script in front of release collections // ========================================================================== if (self.location.href.match(/\/collections/) && document.querySelector("h1").textContent == account.querySelector("span.menu-header").textContent.match(/(\w+)\s.$/)[1]) { var collectionHeaders = document.querySelectorAll("table.tbl > thead > tr"); for (var th = 0; th < collectionHeaders.length; th++) { if (collectionHeaders[th].textContent.match(/Veröffentlichungen|Väljalasked|Releases|Publicaciones|Parutions|Pubblicazioni|Uitgaves|Julkaisut|Κυκλοφορίες|リリース/i)) { collectionHeaders[th].appendChild(createTag("th", {a: {class: "hypermouli-header"}}, [userjs.name.replace(/.+\. /, "") + (self.opera ? " (does not work in Opera!)" : ""), createTag("br"), GM_info.script.description.replace(/.+:/, "")])); var columns = collectionHeaders[th].parentNode.parentNode.querySelectorAll("table.tbl > tbody > tr"); for (var c = 0; c < columns.length; c++) { columns[c].appendChild(createTag("td", {}, [createTag("a", {e: {click: mouli}}, "Put"), " | ", createTag("a", {e: {click: mouli}}, "Delete")])); } } } } // ========================================================================== // ## THE BIG ONE ## goes here // ========================================================================== function mouli() { target = this.parentNode.parentNode.querySelector("a[href*='/collection/']").getAttribute("href").match(re_GUID); method = this.textContent.toLowerCase(); source = prompt("Please paste your release edit search or other collection URL here.\nIt will parse these pages to modify the previously mentionned collection.", localStorage.getItem(userjs.id + "_" + method + "-source") || ""); client = userjs.name.replace(/^mb\. /, "").replace(/ /g, ".").toLowerCase() + "-" + GM_info.script.version; if (target && method.match(/^(put|delete)$/i) && source) { localStorage.setItem(userjs.id + "_" + method + "-source", source); source = source.replace(/^(https?:\/\/[^/]+)?(\/.+)/, "$2"); if (source.match(/\/tag\/[^/]+/)) { source = source.replace(/(^.*\/tag\/[^/]+).*/, "$1/release"); } modal(createTag("h3", {}, [createTag("a", {a: {href: GM_info.script.namespace, target: "_blank"}}, userjs.name), " ", createTag("b", {}, GM_info.script.version), " (reload this page to abort)"])); if (source.match(/^\/search\/edits/)) { modal(createTag("p", {}, "(loading first page of an edit search is always long)")); } loadForExtract(source.replace(/([?&])page=\d+&*/g, "$1") + (source.match(/\?/) ? "&" : "?") + "page=1"); } else { alert("syntax error\ntarget: " + target + "\nmethod: " + method + " (should be either “put” or “delete”)\nsource: " + source); } } function loadForExtract(page) { var xhr = new XMLHttpRequest(); loaders[xhr.getID()] = {maxRetry: 5, url: page}; xhr.addEventListener("load", function(event) { var sPage = loaders[this.getID()].url.match(/page=(\d+)$/)[1]; var iPage = parseInt(sPage, 10); var suffix = {1: "st", 2: "nd", 3: "rd", digit: sPage.match(/(?:^|[02-9])([123])$/)}; sPage = sPage + (suffix.digit ? suffix[suffix.digit[1]] : "th") + " page"; var ploaded = modal(createTag("h4", {}, [createTag("a", {a: {href: loaders[this.getID()].url, target: "_blank"}}, sPage), " loaded"])); document.title = sPage + " loaded (" + userjs.name + ") " + genuineTitle; var res = document.createElement("html"); res.innerHTML = this.responseText; var releases; for (var type in crawlType) if (Object.prototype.hasOwnProperty.call(crawlType, type) && loaders[this.getID()].url.match(new RegExp(type))) { releases = res.querySelectorAll(crawlType[type]); ploaded.appendChild(document.createTextNode(" (" + releases.length + " release" + (releases.length == 1 ? "" : "s") + "):")); if (releases.length > 0) { var url = "/ws/2/collection/" + target + "/releases/"; var cont = modal(createTag("table")); var first = true; for (var r = 0; r < releases.length; r++) { var guid = releases[r].getAttribute("href").match(re_GUID); if (guid) { cont.appendChild(createTag("tr", {}, [createTag("th", {s: {paddingRight: "6px"}}, (r + 1) + "."), createTag("td", {s: {padding: "0px"}}, createTag("img", {a: {src: "http://coverartarchive.org/release/" + guid + "/front-250", alt: ""}, s: {margin: "0px", maxHeight: "16px", maxWidth: "16px", boxShadow: "1px 1px 2px black"}, e: {error: function() { this.parentNode.removeChild(this); }}})), createTag("td", {s: {paddingLeft: "6px"}}, createTag("a", {a: {href: releases[r].getAttribute("href"), target: "_blank"}}, releases[r].textContent))])); if (!first) { url += ";"; } else { first = false; } url += guid; } } requestForAction(method, url + "?client=" + client); } break; } if (res.querySelector("ul.pagination > li:last-of-type > a")) { loadForExtract(page.replace(/(page=)\d+$/, "$1" + (iPage + 1))); } else { document.title = genuineTitle; modal(createTag("h3", {}, ["Last page processed (", createTag("a", {e: {click: function() { self.location.reload(); }}}, "RELOAD"), " page to quit this crap)."])); } }); xhr.addEventListener("error", function(event) { if (--loaders[this.getID()].maxRetry > 0) { modal(createTag("fragment", {}, ["Error loading ", createTag("a", {a: {href: loaders[this.getID()].url, target: "_blank"}}, "page"), ", retrying…"])); loadForExtract(loaders[this.getID()].url); } else { alert("XHR-" + this.getID() + " ERROR " + this.status + "\nStopped retrying.\n" + loaders[this.getID].url + "\n\n" + this.responseText); } }); xhr.openDebug("get", page); xhr.sendDebug(null); } function requestForAction(method, url) { if (self.opera) { modal(createTag("p", {}, ["Will not perform ", createTag("a", {a: {href: url, target: "_blank"}}, method), " (auth-digest does not work in Opera)."])); } else { var xhr = new XMLHttpRequest(); loaders[xhr.getID()] = {method: method, url: url, maxRetry: 5}; xhr.addEventListener("load", function(event) { var node, res = this.responseXML.documentElement; var msg = createTag("fragment", {}, ["Releases “", createTag("a", {a: {title: loaders[this.getID()].method, href: loaders[this.getID()].url, target: "_blank"}}, loaders[this.getID()].method), "” on collection "]); if ((node = res.querySelector("message text")) && node.textContent.match(/ok/i)) { modal(createTag("p", {}, [msg, "OK."])); } else { modal(createTag("p", {}, [msg, "failed.\n\n" + res.textContent])); } }); xhr.addEventListener("error", function(event) { if (--loaders[this.getID()].maxRetry > 0) { modal(createTag("fragment", {}, ["Error performing ", createTag("a", {a: {title: loaders[this.getID()].method, href: loaders[this.getID()].url, target: "_blank"}}, "“" + loaders[this.getID()].method + "” method"), ", retrying…"])); requestForAction(loaders[this.getID()].method, loaders[this.getID()].url); } else { alert("XHR-" + this.getID() + " ERROR " + this.status + "\nStopped retrying.\n" + loaders[this.getID].url + "\n\n" + this.responseText); } }); xhr.openDebug(method, url); xhr.overrideMimeType("text/xml"); xhr.sendDebug(null); } } // ===================================== // ## hacked from COLLECTION HIGHLIGHTER // ===================================== function modal(txt) { var obj = document.getElementById(userjs.id + "modal"); if (txt && !obj) { coolstuff("div", "50", "100%", "100%", "black", ".6"); obj = coolstuff("div", "55", "800px", "600px", "white"); obj.setAttribute("id", userjs.id + "modal"); obj.style.padding = "4px"; obj.style.overflow = "auto"; obj.style.whiteSpace = "nowrap"; obj.style.border = "4px solid black"; obj.addEventListener("mouseover", function(event) { this.style.borderColor = "silver"; }); obj.addEventListener("mouseout", function(event) { this.style.borderColor = "black"; }); } if (txt && obj) { var ret = obj.appendChild(typeof txt == "string" ? document.createTextNode(txt) : txt); if (obj.style.borderColor == "black") { obj.scrollTop = obj.scrollHeight; } return ret; } if (!txt && obj) { obj.parentNode.removeChild(obj.previousSibling); obj.parentNode.removeChild(obj); } function coolstuff(t, z, x, y, b, o) { var truc = document.getElementsByTagName("body")[0].appendChild(document.createElement(t)); truc.style.position = "fixed"; truc.style.zIndex = z; truc.style.width = x; var xx = x.match(/^([0-9]+)(px|%)$/); if (xx) { truc.style.left = ((xx[2] == "%" ? 100 : self.innerWidth) - xx[1]) / 2 + xx[2]; } truc.style.height = y; var yy = y.match(/^([0-9]+)(px|%)$/); if (yy) { truc.style.top = ((yy[2] == "%" ? 100 : self.innerHeight) - yy[1]) / 2 + yy[2]; } if (b) { truc.style.background = b; } if (o) { truc.style.opacity = o; } return truc; } } // =============================================================================== // ## adaptation of JULIEN COUVREUR inspired code at http://oreilly.com/pub/h/4133 // =============================================================================== XMLHttpRequest.prototype.getID = function() { if (!this.id) { this.id = Math.floor(Math.random() * 1000); } return this.id; }; XMLHttpRequest.prototype.openDebug = function(method, url) { var _url = (url.match(/^https?:\/\//) ? "" : self.location.protocol + "//" + self.location.host) + url; if (DEBUG) { console.log("XHR-" + this.getID() + " open " + method.toUpperCase() + " " + _url); } this.open(method, _url); }; XMLHttpRequest.prototype.sendDebug = function(params) { if (DEBUG) { console.log("XHR-" + this.getID() + " send(" + params + ")"); var loaded = function(event) { console.log("XHR-" + this.getID() + " " + event.type + " " + this.status); }; this.addEventListener("load", loaded); this.addEventListener("error", loaded); } this.send(params); };