NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name mb. COLLECTION HIGHLIGHTER // @version 2024.9.4 // @description musicbrainz.org: Highlights releases, release-groups, etc. that you have in your collections (anyone’s collection can be loaded) everywhere // @namespace https://github.com/jesus2099/konami-command // @supportURL https://github.com/jesus2099/konami-command/labels/mb_COLLECTION-HIGHLIGHTER // @downloadURL https://github.com/jesus2099/konami-command/raw/master/mb_COLLECTION-HIGHLIGHTER.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 2012-02-21; https://web.archive.org/web/20140327054755/userscripts.org/scripts/show/126380 / https://web.archive.org/web/20141011084019/userscripts-mirror.org/scripts/show/126380 // @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 GM_deleteValue // @grant GM_getValue // @grant GM_setValue // @include /^https?:\/\/acoustid\.org\/track\// // @include /^https?:\/\/((beta|test)\.)?musicbrainz\.(eu|org)\// // @exclude /^https?:\/\/((beta|test)\.)?musicbrainz\.(eu|org)\/account\// // @exclude /^https?:\/\/((beta|test)\.)?musicbrainz\.(eu|org)\/admin\// // @run-at document-end // ==/UserScript== "use strict"; var DEBUG = localStorage.getItem("jesus2099debug"); let scriptNameAndVersion = GM_info.script.name.substr("4") + " " + GM_info.script.version; // ############################################################################ // # # // # BUTTONS / TRIGGERS / GUI # // # # // ############################################################################ var MBS = self.location.protocol + "//" + self.location.host; var lang = document.querySelector("html[lang]"); lang = lang && lang.getAttribute("lang") || "en-GB"; var cat = self.location.pathname.match(/(area(?!.+(artists|labels|releases|places|aliases|edits))|artist(?!.+(releases|recordings|works|relationships|aliases|edits))|artists|event|labels|releases|recordings|report|series|track|works|aliases|cdtoc|collection(?!s|.+edits)|collections|edit(?!s|\/subscribed)|edits|votes|edit\/subscribed|isrc|label(?!.+edits)|place(?!.+(aliases|edits))|puid|ratings|recording(?!s|.+edits)|relationships|release[-_]group(?!.+edits)|release(?!s|-group|.+edits)|search(?!\/edits)|tracklist|tag|url|work(?!s))/); if (cat) { /* -------- CONFIGURATION START (don’t edit above) -------- */ var highlightColour = "purple"; var skipArtists = [ "89ad4ac3-39f7-470e-963a-56509c546377", // Various Artists "f731ccc4-e22a-43af-a747-64213329e088", // [anonymous] "33cf029c-63b0-41a0-9855-be2a3665fb3b", // [data] "314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc", // [dialogue] "eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61", // [no artist] "a0ef7e1d-44ff-4039-9435-7d5fefdeecc9", // [theatre] "9be7f096-97ec-4615-8957-8d40b5dcbc41", // [traditional] "125ec42a-7229-4250-afc5-e057484327fe", // [unknown] ]; var skipLabels = [ "157afde4-4bf5-4039-8ad2-5a15acc85176", // [no label] "46caaa9e-3e26-49b5-827c-64ccc73c1b07", // [unknown] ]; var MBWSRate = 999; var MBWSSpeedLimit = 100; // from 1 (only 1 result per request) to 100 (maxium amount of result per request) /* -------- CONFIGURATION END (don’t edit below) -------- */ var prefix = "collectionHighlighter"; var dialogprefix = "..:: " + scriptNameAndVersion.replace(/ /g, " :: ") + " ::..\n\n"; var maxRetry = 20; var retryPause = 5000; var slowDownStepAfterRetry = 0; var css_nextPage = "ul.pagination > li:last-of-type > a"; var retry = 0; var lowMemory = navigator.deviceMemory && navigator.deviceMemory <= 1; // 1 (GB) on my Raspberry Pi 3B+ var j2ss = document.createElement("style"); j2ss.setAttribute("type", "text/css"); document.head.appendChild(j2ss); j2ss = j2ss.sheet; var brdr = " border-left: 4px solid " + highlightColour + "; "; j2ss.insertRule("." + prefix + "Box {" + brdr.replace(/-left/, "") + "}", 0); j2ss.insertRule("." + prefix + "Row {" + brdr + "}", 0); j2ss.insertRule("li." + prefix + "Row { padding-left: 4px; }", 0); j2ss.insertRule("." + prefix + "Item { text-shadow: 0 0 8px " + highlightColour + "!important; }", 0); var collectionsID = GM_getValue("collections") || ""; var releaseID; var stuff = {}; var collectedStuff = ["collection", "release", "release-group", "recording", "artist", "work", "label"]; var strType = "release-group|recording|label|artist|work"; var strMBID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; var collectionLoadingStartDate; var currentTaskStartDate; cat = cat[1].replace(/edit\/subscribed|votes/, "edits").replace(/_/, "-"); debug("CAT: " + cat); // ############################################################################ // # DISPLAY RELEASE PAGE SIDEBAR TOOLS # // ############################################################################ if (cat == "release" && (releaseID = self.location.pathname.match(new RegExp(strMBID)))) { releaseID = releaseID[0]; var mainReleasePage = self.location.pathname.match(new RegExp("^/release/" + strMBID + "$")); var colls = document.querySelectorAll("div#sidebar a[href*='/collection_collaborator/add?release='], div#sidebar a[href*='/collection_collaborator/remove?release=']"); for (let coll = 0; coll < colls.length; coll++) { if (collectionsID.indexOf(colls[coll].getAttribute("href").match(new RegExp(strMBID))) > -1) { if (mainReleasePage) { collectionUpdater(colls[coll], colls[coll].getAttribute("href").match(/add|remove/).toString()); } else { addAfter(createTag("div", {s: {textShadow: "0 0 8px " + highlightColour}}, ["(please go to ", createTag("a", {a: {href: "/release/" + releaseID}}, "main release page"), " for this button to auto‐update your collection highlighter)"]), colls[coll]); } } } /* if (mainReleasePage) { // TODO: Feature hidden as long as it is not working (will not add any elements of releases already partially highlighted) var lili = document.querySelector("div#sidebar > h2.collections ~ ul.links"); if (lili) { var buttxt = " this release to your local collection highlighter,\nwithout changing its status among you MB collection(s)"; lili = lili.insertBefore(document.createElement("li"), lili.firstChild); lili.appendChild(document.createTextNode("Force highlight ")); collectionUpdater(lili.appendChild(createA("ON", self.location.href, "Add" + buttxt, true)), "add"); lili.appendChild(document.createTextNode(" / ")); collectionUpdater(lili.appendChild(createA("OFF", self.location.href, "Remove" + buttxt.replace(" to ", " from "), true)), "remove"); } } */ } if (cat == "collections") { // ############################################################################ // # DISPLAY COLLECTION PAGE LOADER TOOLS # // ############################################################################ var xp1 = document.evaluate("//xhtml:table[contains(@class, 'tbl')]/xhtml:thead//xhtml:th/text()[contains(., 'Veröffentlichungen') or contains(., 'Väljalasked') or contains(., 'Releases') or contains(., 'Publicaciones') or contains(., 'Parutions') or contains(., 'Pubblicazioni') or contains(., 'Uitgaves') or contains(., 'Julkaisut') or contains(., 'Κυκλοφορίες') or contains(., 'リリース')]", document, nsr, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let tbl_idx = 0; tbl_idx < xp1.snapshotLength > 0; tbl_idx++) { xp1.snapshotItem(tbl_idx).parentNode.parentNode.appendChild(createTag("th", {a: {colspan: "2"}}, scriptNameAndVersion)); var tbl = getParent(xp1.snapshotItem(tbl_idx).parentNode, "table"); var xp = document.evaluate("./xhtml:tbody/xhtml:tr", tbl, nsr, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < xp.snapshotLength; i++) { let coll = xp.snapshotItem(i).getElementsByTagName("a")[0]; var collid = coll.getAttribute("href").match(new RegExp(strMBID)); var loadButtons = []; var loadButtonText = "Load"; if (collectionsID.indexOf(collid) > -1) { decorate(coll); loadButtonText = "Reload"; } loadButtons.push(createA( "Append", function(event) { var opts = document.querySelectorAll("td." + prefix + " input[type='checkbox']:checked"); stuff = {}; for (let opt = 0; opt < opts.length; opt++) { stuff[opts[opt].getAttribute("name")] = {}; } collectionLoadingStartDate = Date.now(); currentTaskStartDate = Date.now(); loadCollection(this.getAttribute("title").match(new RegExp(strMBID)), "add"); }, "Add this collection’s content to highlighter (" + collid + ")" )); loadButtons.push(" | "); loadButtons.push(createA( loadButtonText, function(event) { var cmsg = "This will REPLACE your current loaded stuff."; if (confirm(dialogprefix + cmsg)) { // erase local stuff for (let stu = 0; stu < collectedStuff.length; stu++) { GM_deleteValue(collectedStuff[stu] + "s"); } // then append (previous button) this.previousSibling.previousSibling.click(); } }, "Replace current highlighted content with this collection’s content (" + collid + ")" )); xp.snapshotItem(i).appendChild(document.createElement("td")).appendChild(concat(loadButtons)); if (i == 0) { /* settings */ var settings = []; for (let stu = 0; stu < collectedStuff.length; stu++) if (collectedStuff[stu] != "collection") { var cstuff = collectedStuff[stu]; var lab = document.createElement("label"); lab.appendChild(concat([createTag("input", {a: {type: "checkbox", name: cstuff}, e: {change: function(event) { GM_setValue("cfg" + this.getAttribute("name"), this.checked ? "1" : "0"); }}}), cstuff + "s "])); var cfgcb = lab.querySelector("input[type='checkbox'][name='" + cstuff + "']"); if (cstuff.match(/artist|recording|release(-group)?|work/)) { // defaults cfgcb.setAttribute("checked", "checked"); } if (cstuff.match(/release(-group)?/)) { // forced lab.style.setProperty("opacity", ".5"); cfgcb.setAttribute("disabled", "disabled"); } else { // read previous settings var cfgstu = GM_getValue("cfg" + cstuff); if (cfgstu == "1") { cfgcb.setAttribute("checked", "checked"); } else if (cfgstu == "0") { cfgcb.removeAttribute("checked"); } } if (cstuff.match(/artist|work/)) { // artist and work tracking requires recording tracking cfgcb.addEventListener("change", function(event) { if (this.checked) { var recording = this.parentNode.parentNode.querySelector("input[name='recording']"); recording.checked = true; sendEvent(recording, "change"); } }, false); } else if (cstuff.match(/recording/)) { cfgcb.addEventListener("change", function(event) { if (!this.checked) { var artistwork = this.parentNode.parentNode.querySelectorAll("input[name='artist'], input[name='work']"); for (let aw = 0; aw < artistwork.length; aw++) { artistwork[aw].checked = false; sendEvent(artistwork[aw], "change"); } } }, false); } settings.push(lab); settings.push(document.createElement("br")); } xp.snapshotItem(i).appendChild(createTag("td", {a: {class: prefix, rowspan: xp.snapshotLength}}, concat(settings))); } } } // ############################################################################ // # LAUNCH THE HIGHLIGHTING ON ALL PAGES # // ############################################################################ } else if (cat == "track" /* AcoustID */) { // AcoustID.org: no problems findOwnedStuff(); } else { // MusicBrainz.org: React problems var DOMChanged = true; (new MutationObserver(function(mutations, observer) { for (var m = 0; m < mutations.length; m++) { if (mutations[m].type === "childList") { DOMChanged = true; } } })).observe(document.querySelector("div#content, div#page"), {childList: true, subtree: true}); setInterval(function() { // Make sure to re-scan page content (after, not during) each time React does some funny stuff hydrate redraw content if (!document.querySelector("p.loading-message") && DOMChanged) { DOMChanged = false; findOwnedStuff(); } }, 1000); } } // ############################################################################ // # # // # MAIN FUNCTIONS # // # # // ############################################################################ // ############################################################################ // # FIND COLLECTION ENTITIES IN THE PAGE # // ############################################################################ function findOwnedStuff() { // Annotation link trim spaces and protocol + "//" + host for (let annotationLinks = document.querySelectorAll("div#content div.annotation a, div#content div.annotation-preview a"), l = 0; l < annotationLinks.length; l++) { annotationLinks[l].setAttribute("href", annotationLinks[l].getAttribute("href").trim().replace(/^((https?:)?\/\/((beta|test)\.)?musicbrainz\.org)\//, "/")); } for (let stu = 0; stu < collectedStuff.length; stu++) { var cstuff = collectedStuff[stu]; stuff[cstuff] = {}; var uphill = ""; var downhill = cat == "release" && cstuff == "label" ? "" : "[count(ancestor::xhtml:div[contains(@id, 'sidebar')])=0]"; var root = cat == "track" /* acoustid.org */ ? "//musicbrainz.org/" : "/"; var path = uphill + "//xhtml:a[contains(@href, '" + root + cstuff + "/')][not(starts-with(@class, '" + prefix + "'))]" + downhill; var xp = document.evaluate(path, document, nsr, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < xp.snapshotLength; i++) { var mbid = xp.snapshotItem(i).getAttribute("href").match(new RegExp("/" + cstuff + "/(" + strMBID + ")$")); if (mbid) { mbid = mbid[1]; debug(cstuff + " is already loaded? " + stuff[cstuff].loaded); if (!stuff[cstuff].loaded) { debug("Load " + cstuff); stuff[cstuff].rawids = GM_getValue(cstuff + "s"); if (stuff[cstuff].rawids) { stuff[cstuff].ids = stuff[cstuff].rawids.trim().split(" "); debug(" \n" + stuff[cstuff].ids.length + " " + cstuff.toUpperCase() + (stuff[cstuff].ids.length == 1 ? "" : "S") + " loaded (" + cstuff + "s)\nMatching: " + path); } else { debug(" \nNo " + cstuff.toUpperCase() + "S in highlighter (" + cstuff + "s)"); } stuff[cstuff].loaded = true; debug(cstuff + " is now loaded? " + stuff[cstuff].loaded); } if (stuff[cstuff].ids && stuff[cstuff].ids.indexOf(mbid) > -1) { debug(mbid + " ● “" + xp.snapshotItem(i).textContent + "”"); decorate(xp.snapshotItem(i)); } } } } } // ############################################################################ // # HIGHLIGHT / DECORATE A LINK # // ############################################################################ function decorate(entityLink) { if (!getParent(entityLink, "div", "tabs")) { // Does not highlight tabs. entityLink.classList.add(prefix + "Item"); var page = document.getElementById("page"); if (getParent(entityLink, "h1")) { // Entity page is boxed. page.classList.add(prefix + "Box"); } else { var entityType = entityLink.getAttribute("href").match(new RegExp("/(.+?)/" + strMBID, "i")) || ""; if (entityType) { entityType = entityType[1]; } if (cat == "edit") { // entity edit page is boxed var editDetails = getParent(entityLink, "table", "details"); if (editDetails && entityLink == editDetails.querySelector("a")) { page.classList.add(prefix + "Box"); } } else if (cat == "edits") { // in edit lists: Release or release group edits are boxed; other entity edits are left bordered var edit = getParent(entityLink, "div", "edit-list"); if (edit) { edit.classList.add(prefix + (entityLink == edit.querySelector("div.edit-details a") ? "Box" : "Row")); } } // Associated tracks are Leftmost entity table rows are left bordered. Not in owned release tracklists var row = !getParent(entityLink, "ul") && !getParent(entityLink, "dl") && getParent(entityLink, "tr"); if (row) { if ( entityLink == row.querySelector("a[href]:not([href*='coverartarchive.org']):not([href*='/track/']):not([href$='/cover-art'])" + (cat == "recording" ? ":not([href^='/artist/'])" : "")) && !(cat == "cdtoc" && entityType == "recording") && !(cat == "release" && page.classList.contains(prefix + "Box") && entityType == "recording") && cat != "aliases" ) { row.classList.add(prefix + "Row"); } // decorate tracks without holding them if (cat == "release" && entityType == "recording" || cat == "recording" && entityType == "release") { var track = row.querySelector("a[href*='/track/']"); if (track) { track.classList.add(prefix + "Item"); } } } } } } // ############################################################################ // # COLLECT RELEASES FROM COLLECTION # // ############################################################################ function loadCollection(collectionMBID, action) { setTitle(true); // Add collection MBID to list of highlighted collectionsID = GM_getValue("collections") || ""; if (collectionsID.indexOf(collectionMBID) < 0) { collectionsID += collectionMBID + " "; } modal(true, concat([createTag("h3", {}, dialogprefix), "WTF? If you want to stop this monster crap, just ", createA("reload", function(event) { self.location.reload(); }), " or close this page.", "<br>", "<br>", "<hr>", "Loading collection " + collectionMBID + "…"]), 2); for (let stu in stuff) if (Object.prototype.hasOwnProperty.call(stuff, stu) && collectedStuff.indexOf(stu) >= 0) { stuff[stu].rawids = GM_getValue(stu + "s") || ""; } stuff["release-new"] = {ids: []}; stuff["missingRecordingWorks"] = []; if (lowMemory) { modal(true, "Low memory machine (" + navigator.deviceMemory + " GB): Displaying less stuff to preserve performances.", 2); } loadReleases("?collection=" + collectionMBID, action, concludeCollectionLoading); } function loadReleases(query /* "?collection=MBID&" or "/MBID?" */, action /* add or remove */, conclusionCallback, _offset) { var offset = _offset || 0; var ws2releaseUrl = "/ws/2/release" + query + (query.indexOf("?") >= 0 ? "&" : "?") + "inc=release-groups+release-group-level-rels+release-group-rels+labels+recordings+artist-credits+recording-level-rels+work-rels+recording-rels&limit=" + MBWSSpeedLimit + "&offset=" + offset; if (offset == 0 || !lowMemory) { modal(true, "Fetching releases…", 1); } var xhr = new XMLHttpRequest(); xhr.addEventListener("load", function(event) { if (this.status == 401) { error(concat(["Error 401. Please ", createA("report bug", GM_info.script.supportURL), " to ", GM_info.script.author, "."])); } else if (this.status == 200) { var oneRelease = this.response.releases === undefined; if (!lowMemory) { modal(true, "Received " + (oneRelease ? 1 : this.response.releases.length.toLocaleString(lang)) + " release" + ((oneRelease ? 1 : this.response.releases.length) == 1 ? "" : "s") + ":", 2); } browseReleases(oneRelease ? [this.response] : this.response.releases, action, offset, oneRelease ? 1 : this.response["release-count"]); if (!lowMemory) { modal(true, "", 1); } var newOffset = !oneRelease && this.response["release-offset"] + this.response.releases.length; if (!oneRelease && newOffset < this.response["release-count"]) { retry = 0; setTimeout(function() { loadReleases(query, action, conclusionCallback, newOffset); }, chrono(MBWSRate)); } else { // end of recursive function if (stuff["release-new"].ids.length > 0) { delete(stuff["release-new"]); // free up memory if (stuff["missingRecordingWorks"].length > 0) { modal(true, concat(["<hr>", "\u26A0\uFE0F Big releases require ", createTag("b", {s: {color: highlightColour}}, "delayed work fetching"), " (for " + stuff["missingRecordingWorks"].length.toLocaleString(lang) + " recordings):"]), 2); retry = 0; setTimeout(function() { currentTaskStartDate = Date.now(); loadMissingRecordingWorks(stuff["missingRecordingWorks"], action, conclusionCallback); delete(stuff["missingRecordingWorks"]); // free up memory }, chrono(MBWSRate)); } else { conclusionCallback(); } } else { modal(true, "No new releases.", 2); unlockModal(); } } } else { if (retry++ < maxRetry) { MBWSRate += slowDownStepAfterRetry; modal(true, "Error " + this.status + " “" + this.statusText + "” (" + retry + "/" + maxRetry + ")", 2); debugRetry(this.status); setTimeout(function() { loadReleases(query, action, conclusionCallback, offset); }, chrono(retryPause)); } else { error("Too many (" + maxRetry + ") errors (last " + this.status + " “" + this.statusText + "” while loading collection)."); } } }); debug(MBS + ws2releaseUrl); chrono(); xhr.open("GET", MBS + ws2releaseUrl, true); xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); xhr.send(null); } // ############################################################################ // # COLLECT ALL DATA FROM A SET OF RELEASES # // ############################################################################ function browseReleases(releases, action, offset, releaseCount) { for (var r = 0; r < releases.length; r++) { var country = releases[r].country ? createTag("span", {a: {class: "flag flag-" + releases[r].country}}) : ""; var disambiguation = releases[r].disambiguation ? createTag("span", {a: {class: "comment"}}, "(" + releases[r].disambiguation + ")") : ""; if (!lowMemory) { modal(true, concat([createTag("code", {s: {whiteSpace: "pre", textShadow: "0 0 8px " + highlightColour}}, (offset + r + 1).toLocaleString(lang).padStart(6, " ")), ". ", country, createA(releases[r].title, "/release/" + releases[r].id), " ", disambiguation]), 1, {text: "releases", current: offset + r + 1, total: releaseCount}); } else { modal(true, "", 0, {text: "releases", current: offset + r + 1, total: releaseCount}); } var missingRecordingLevelRels = 0; if (stuff["release"].rawids.indexOf(releases[r].id) < 0) { stuff["release-new"].ids.push(releases[r].id); } addRemoveEntities("release", releases[r], action); if (stuff["artist"]) { addRemoveEntities("artist", releases[r]["artist-credit"], action); } if (stuff["label"]) { addRemoveEntities("label", releases[r]["label-info"], action); } addRemoveEntities("release-group", releases[r]["release-group"], action); for (var rgRel = 0; rgRel < releases[r]["release-group"].relations.length; rgRel++) { // add included release groups and their artists if ( ( releases[r]["release-group"].relations[rgRel].type == "included in" || releases[r]["release-group"].relations[rgRel]["type-id"] == "589447ea-be2c-46cc-b9e9-469e1d06e18a" ) && releases[r]["release-group"].relations[rgRel].direction == "backward" || ( releases[r]["release-group"].relations[rgRel].type == "excerpt from" || releases[r]["release-group"].relations[rgRel]["type-id"] == "6d3242b9-0eb5-45c7-bf07-303aaff46d0f" ) && releases[r]["release-group"].relations[rgRel].direction == "backward" ) { if (!lowMemory) { var excerpt = releases[r]["release-group"].relations[rgRel].type.match(/excerpt/); excerpt = excerpt ? excerpt[0] + " " : ""; modal(true, concat([createTag("code", {s: {whiteSpace: "pre", color: "grey"}}, "\t└"), " Includes " + excerpt, createA(releases[r]["release-group"].relations[rgRel].release_group.title, "/release-group/" + releases[r]["release-group"].relations[rgRel].release_group.id)]), 1); } addRemoveEntities("release-group", releases[r]["release-group"].relations[rgRel].release_group, action); if (stuff["artist"]) { addRemoveEntities("artist", releases[r]["release-group"].relations[rgRel].release_group["artist-credit"], action); } } } addRemoveEntities("release-group", releases[r]["release-group"], action); if (stuff["artist"]) { addRemoveEntities("artist", releases[r]["release-group"]["artist-credit"], action); } for (var m = 0; m < releases[r].media.length; m++) { if (releases[r].media[m].pregap) { missingRecordingLevelRels += browseTrack(releases[r].media[m].pregap, action); } if (releases[r].media[m].tracks) for (var t = 0; t < releases[r].media[m].tracks.length; t++) { missingRecordingLevelRels += browseTrack(releases[r].media[m].tracks[t], action); } if (releases[r].media[m]["data-tracks"]) for (var dt = 0; dt < releases[r].media[m]["data-tracks"].length; dt++) { missingRecordingLevelRels += browseTrack(releases[r].media[m]["data-tracks"][dt], action); } } if (!lowMemory && missingRecordingLevelRels > 0) { modal(true, concat([createTag("code", {s: {whiteSpace: "pre", color: "grey"}}, "\t└"), " \u26A0\uFE0F " + missingRecordingLevelRels.toLocaleString(lang) + " recordings queued for ", createTag("b", {s: {color: highlightColour}}, "delayed work fetching")]), 1); } } } function browseTrack(track, action) { var missingRecordingLevelRels = 0; if (stuff["artist"]) { addRemoveEntities("artist", track["artist-credit"], action); } if (stuff["recording"]) { addRemoveEntities("recording", track.recording, action); } if (stuff["artist"]) { addRemoveEntities("artist", track.recording["artist-credit"], action); } if (stuff["recording"] || stuff["work"]) { if (track.recording.relations) { for (var r = 0; r < track.recording.relations.length; r++) { // add works if ( stuff["work"] && ( track.recording.relations[r].type === "performance" || track.recording.relations[r]["type-id"] === "a3005666-a872-32c3-ad06-98af558e99b0" ) // strange MBS-12714 bug && track.recording.relations[r]["target-type"] == "work" && track.recording.relations[r].work ) { addRemoveEntities("work", track.recording.relations[r].work, action); } // add compiled recordings and their artists if ( stuff["recording"] && ( track.recording.relations[r].type === "compilation" || track.recording.relations[r]["type-id"] === "1b6311e8-5f81-43b7-8c55-4bbae71ec00c" ) && track.recording.relations[r].direction == "forward" // in case of another MBS-12714 && track.recording.relations[r]["target-type"] == "recording" && track.recording.relations[r].recording ) { if (!lowMemory) { modal(true, concat([createTag("code", {s: {whiteSpace: "pre", color: "grey"}}, "\t└"), " Compiles ", createA(track.recording.relations[r].recording.title, "/recording/" + track.recording.relations[r].recording.id)]), 1); } addRemoveEntities("recording", track.recording.relations[r].recording, action); if (stuff["artist"]) { addRemoveEntities("artist", track.recording.relations[r].recording["artist-credit"], action); } } } } else { // no recording.relations: when there are more than 500 tracks, the recording-level-rels are not returned if (stuff["missingRecordingWorks"].indexOf(track.recording.id) < 0) { // add each recording to a list for later later work fetch stuff["missingRecordingWorks"].push(track.recording.id); missingRecordingLevelRels += 1; } } } return missingRecordingLevelRels; } // ############################################################################ // # UPDATE STUFF # // ############################################################################ function addRemoveEntities(type, _entities, action) { var entities = Array.isArray(_entities) ? _entities : [_entities]; for (var e = 0; e < entities.length; e++) { debug(action + " " + type + "\n" + JSON.stringify(entities[e])); var entity = {}; switch (type) { case "artist": // fall through case "label": if (entities[e][type]) { entity.id = entities[e][type].id; entity.name = entities[e][type].name; } else { // labels can be null entity = null; } break; case "recording": // fall through case "release": // fall through case "release-group": // fall through case "work": entity.id = entities[e].id; entity.name = entities[e].title; break; } debug(type); debug(stuff[type]); if ( entity !== null // labels can be null && stuff[type] // this type is highlighted (initial load) && typeof stuff[type].rawids == "string" // this type is highlighted (dynamic load) && stuff[type].rawids.indexOf(entity.id) < 0 // this entity is not yet tracked && !(type == "artist" && skipArtists.indexOf(entity.id) >= 0) // ignore Various Artists and such && !(type == "label" && skipLabels.indexOf(entity.id) >= 0) // ignore [no label] and such ) { switch (action) { case "add": stuff[type].rawids += entity.id + " "; break; case "remove": // TODO: remove entity break; } } } } // ############################################################################ // # LOAD WORKS FROM HUNDREDS OF RECORDINGS # // ############################################################################ var mbs12154 = 0; // #### REMOVE WHEN MBS-12154 FIXED // reduce batch size (results in random order, we try to get less than 101 results to keep them all on one page) function loadMissingRecordingWorks(recordings, action, conclusionCallback, _batchOffset, _wsResponseOffset) { var batchOffset = _batchOffset || 0; var wsResponseOffset = _wsResponseOffset || 0; // keep the query URL short enough (100 recordings) to avoid 414 Request-URI Too Large var batchSize = 100; batchSize -= mbs12154; // #### REMOVE WHEN MBS-12154 FIXED var batch = recordings.slice(batchOffset, batchOffset + batchSize); var workQueryURL = "/ws/2/work?query=rid%3A" + batch.join("+OR+rid%3A") + "&limit=" + MBWSSpeedLimit + "&offset=" + wsResponseOffset; if (wsResponseOffset === 0) { modal(true, "Fetching works for " + batch.length + " recordings… ", 0); } var xhr = new XMLHttpRequest(); xhr.addEventListener("load", function(event) { if (this.status == 200) { var works = this.response.works; modal(true, works.length.toString(), 0, {text: "recordings", current: batchOffset + batch.length, total: recordings.length}); if (stuff["work"]) { for (var r = 0; r < works.length; r++) { addRemoveEntities("work", works[r], action); } } var newWsResponseOffset = this.response.offset + works.length; if (newWsResponseOffset < this.response.count) { modal(true, concat([createTag("span", {s: {color: "grey"}}, "+"), " works"]), 0); mbs12154 += this.response.count - MBWSSpeedLimit; // #### REMOVE WHEN MBS-12154 FIXED if (mbs12154 < MBWSSpeedLimit) { // #### REMOVE WHEN MBS-12154 FIXED modal(true, concat(["<br>", "<br>", createTag("b", {s: {color: highlightColour}}, ["Slow down required (", createTag("a", {a: {href: "https://tickets.metabrainz.org/browse/MBS-12154", target: "_blank"}}, "MBS-12154"), ")"]), "<br>", "Reducing recording query size: ", createTag("del", {}, batchSize), "→", createTag("b", {}, batchSize - this.response.count + MBWSSpeedLimit), " recordings"]), 2); // #### REMOVE WHEN MBS-12154 FIXED retry = 0; // #### UNCOMMENT WHEN MBS-12154 FIXED // setTimeout(function() { loadMissingRecordingWorks(recordings, action, conclusionCallback, batchOffset, newWsResponseOffset); }, chrono(MBWSRate)); setTimeout(function() { loadMissingRecordingWorks(recordings, action, conclusionCallback, batchOffset); }, chrono(MBWSRate)); // #### REMOVE WHEN MBS-12154 FIXED } else { // #### REMOVE WHEN MBS-12154 FIXED error("MBS-12154 bug\n\nCannot load works."); // #### REMOVE WHEN MBS-12154 FIXED } // #### REMOVE WHEN MBS-12154 FIXED } else { modal(true, " works", 1); var newBatchOffset = batchOffset + batch.length; if (newBatchOffset < recordings.length) { retry = 0; setTimeout(function() { loadMissingRecordingWorks(recordings, action, conclusionCallback, newBatchOffset); }, chrono(MBWSRate)); } else { // end of recursive function modal(true, "", 1); conclusionCallback(); } } } else { if (retry++ < maxRetry) { MBWSRate += slowDownStepAfterRetry; modal(true, "Error " + this.status + " “" + this.statusText + "” (" + retry + "/" + maxRetry + ")", 2); debugRetry(this.status); setTimeout(function() { loadMissingRecordingWorks(recordings, action, conclusionCallback, batchOffset, wsResponseOffset); }, chrono(retryPause)); } else { error("Too many (" + maxRetry + ") errors (last " + this.status + " “" + this.statusText + "” while loading missing recording works)."); } } }); debug(MBS + workQueryURL + "&fmt=json"); chrono(); xhr.open("GET", MBS + workQueryURL, true); xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); xhr.send(null); } // ############################################################################ // # DYNAMIC COLLECTION UPDATER (FOR CURRENT RELEASE) # // ############################################################################ var altered = false; function collectionUpdater(link, action) { if (link && action) { link.addEventListener("click", function(event) { modal(true, "Refreshing memory…", 1); collectionsID = GM_getValue("collections") || ""; for (let type in stuff) if (Object.prototype.hasOwnProperty.call(stuff, type) && collectedStuff.indexOf(type) >= 0) { stuff[type].rawids = GM_getValue(type + "s"); stuff[type].ids = stuff[type].rawids != null ? (stuff[type].rawids.length > 0 ? stuff[type].rawids.trim().split(" ") : []) : null; } setTitle(true); lastLink(this.getAttribute("href")); switch (action) { case "add": stuff["release-new"] = {ids: []}; stuff["missingRecordingWorks"] = []; collectionLoadingStartDate = Date.now(); currentTaskStartDate = Date.now(); loadReleases("/" + releaseID, action, concludeDynamicReleaseLoading); return stop(event); // break; case "remove": // Force release for "Add to / Remove from collection" but not for "Force highlight ON /OFF" altered = this.getAttribute("href") != self.location.href; var checks = getStuffs(); if (stuff["release"].rawids.indexOf(releaseID) > -1) { modal(true, concat([createTag("b", {}, "Removing this release"), " from loaded collection…"]), 1); stuff["release"].rawids = stuff["release"].rawids.replace(new RegExp(releaseID + "( |$)"), ""); GM_setValue("releases", stuff["release"].rawids); altered = true; } if (checks.length > 0) { lastLink(this.getAttribute("href")); stuffRemover(checks); return stop(event); } break; } if (!altered) { modal(true, "Nothing has changed.", 1); setTimeout(function() { modal(false); }, 1000); return stop(event); } else { modal(true, "Re‐loading page…", 1); } }, false); decorate(link); } } // ############################################################################ // # COLLECT ALL DATA FROM CURRENT RELEASE # // ############################################################################ function getStuffs(what, pwhere) { var cont = pwhere ? pwhere : document; var selector = { "release": "div#content table.tbl > tbody > tr > td a[href^='/release/']", // pwhere(lab,rec,rgr) "release-group": "div.releaseheader a[href^='/release-group/']", // rel "recording": (pwhere ? "div#content [href^='/recording/']" : "table.medium > tbody > tr > td:not(.pos):not(.video) > a[href^='/recording/'], table.medium > tbody > tr > td:not(.pos):not(.video) > :not(div):not(.ars) a[href^='/recording/']"), // pwhere(art,wrk)/rel "artist": "div.releaseheader a[href^='/artist/'], div#content table.tbl > tbody > tr > td > a[href^='/artist/'], div#content table.tbl > tbody > tr > td > span > a[href^='/artist/'], div#content table.tbl > tbody > tr > td > span > span > a[href^='/artist/']", // rel "work": "div#content div.ars > dl.ars > dd > a[href^='/work/'], div#content div.ars > dl.ars > dd > span.mp > a[href^='/work/'], div#content div.bottom-credits > table.details > tbody > tr > td > a[href^='/work/'], div#content div.bottom-credits > table.details > tbody > tr > td > span.mp > a[href^='/work/']", // rel "label": "div#sidebar > ul.links > li a[href^='/label/']", // rel }; if (what) { return cont.querySelectorAll(selector[what]); } else { var allrelsel = selector["release-group"]; if (stuff["recording"].ids) { allrelsel += ", " + selector["recording"]; } if (stuff["label"].ids) { allrelsel += ", " + selector["label"]; } var all = basicOnly(cont.querySelectorAll(allrelsel)); var allrecsel = "", sep = ""; if (stuff["artist"].ids) { allrecsel += sep + selector["artist"]; sep = ", "; } if (stuff["work"].ids) { allrecsel += sep + selector["work"]; } if (allrecsel != "") { all = basicOnly(cont.querySelectorAll(allrecsel), all); } return all; } function pathname(href) { return href.match(new RegExp("/[^/]+/" + strMBID)) + ""; } function basicOnly(nodes, parr) { var arr = parr ? parr : []; var hrefs = []; for (let a = 0; a < arr.length; a++) { hrefs.push(pathname(arr[a].getAttribute("href"))); } for (let n = 0; n < nodes.length; n++) { if (nodes[n].getAttribute) { var href = pathname(nodes[n].getAttribute("href")); if (href && href.match(new RegExp(strMBID + "$")) && hrefs.indexOf(href) == -1) { hrefs.push(href); arr.push(nodes[n]); } } } return arr; } } // ############################################################################ // # CHECK IF STUFF IS STILL USED # // ############################################################################ function stuffRemover(checks, pp) { var check = checks[0]; if (check) { var p = pp ? pp : 1; var checkMatch = check.getAttribute("href").match(new RegExp("/(" + strType + ")/(" + strMBID + ")$", "i")); if (checkMatch) { var checkType = checkMatch[1]; var checkID = checkMatch[2]; var checkAgainst; switch (checkType) { case "label": case "recording": case "release-group": checkAgainst = "release"; break; case "artist": case "work": checkAgainst = "recording"; break; } if (typeof stuff[checkAgainst].rawids == "string" && stuff[checkType].rawids.indexOf(checkID) > -1) { var url = "/" + checkType + "/" + checkID; if (checkType == "artist") { url += "/recordings"; } url += "?page=" + p; modal(true, concat(["Checking " + checkType + " ", createA(checkType != "release-group" ? check.getAttribute("jesus2099userjs81127recname") || check.textContent : checkID, check.getAttribute("href"), checkType), " against all its ", createA(checkAgainst + "s" + (p > 1 ? " (page " + p + ")" : ""), url), "…"]), 1); // jesus2099userjs81127recname linked to mb_INLINE-STUFF var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(event) { if (this.readyState == 4) { if (this.status == 200) { var res = document.createElement("html"); res.innerHTML = this.responseText; var nextPage = res.querySelector(css_nextPage); var mates = getStuffs(checkAgainst, res); for (let m = 0; m < mates.length; m++) { if (stuff[checkAgainst].rawids.indexOf(mates[m].getAttribute("href").match(new RegExp(strMBID))) > -1) { modal(true, createTag("span", {s: {color: "grey"}}, ["still used by ", createA(mates[m].textContent, mates[m].getAttribute("href"), checkAgainst), ": keeping."]), 1); retry = 0; setTimeout(function() { stuffRemover(checks.slice(1)); }, chrono(MBWSRate)); return; } } if (nextPage) { if (p == 1) { modal(true, createTag("span", {s: {color: "grey"}}, "(several pages but this check will stop as soon as it finds something)"), 1); } retry = 0; setTimeout(function() { stuffRemover(checks, p + 1); }, chrono(MBWSRate)); } else { modal(true, concat([createTag("span", {s: {color: "grey"}}, "not used any more: "), createTag("b", {}, "removing"), "…"]), 1); stuff[checkType].rawids = stuff[checkType].rawids.replace(new RegExp(checkID + "( |$)"), ""); GM_setValue(checkType + "s", stuff[checkType].rawids); altered = true; retry = 0; setTimeout(function() { stuffRemover(checks.slice(1)); }, chrono(MBWSRate)); } } else { if (retry++ < maxRetry) { MBWSRate += slowDownStepAfterRetry; debugRetry(this.status); setTimeout(function() { stuffRemover(checks, p); }, chrono(retryPause)); } else { error("Too many (" + maxRetry + ") errors (last " + this.status + " while checking stuff to remove)."); } } } // 4 }; debug(MBS + url); chrono(); xhr.open("GET", MBS + url, true); xhr.send(null); } else { if (!stuff[checkAgainst].rawids) {/* Protection for some edge cases of new script using old script data */ modal(true, concat([createTag("span", {s: {color: "grey"}}, ["no ", checkAgainst, "s at all (", createA("#87", "//github.com/jesus2099/konami-command/issues/87"), "): "]), "removing…"]), 1); stuff[checkType].rawids = stuff[checkType].rawids.replace(new RegExp(checkID + "( |$)"), ""); GM_setValue(checkType + "s", stuff[checkType].rawids); altered = true; } retry = 0; setTimeout(function() { stuffRemover(checks.slice(1)); }, chrono(MBWSRate)); } } } else { // end of recursive function setTitle(false); if (altered) { lastLink(); } else { modal(false); } } } // ############################################################################ // # # // # ACCESSORY DISPLAY FUNCTIONS # // # # // ############################################################################ function setTitle(ldng, percentage) { var old = document.title.match(/ :: (.+)$/); old = old ? old[1] : document.title; if (ldng) { document.title = (percentage ? percentage + "%" : "⌛") + " Altering highlighter content… :: " + old; } else { document.title = old; } } function concludeCollectionLoading() { saveRawIdsToGMStorage(); unlockModal(); } function concludeDynamicReleaseLoading() { modal(true, "Re‐loading page…", 1); saveRawIdsToGMStorage(); lastLink(); } function saveRawIdsToGMStorage() { setTitle(false); modal(true, concat(["<hr>", createTag("h2", {}, "Highlighted stuff")]), 1); // display summary of added entities and write all MBID in the storage now for (let type in stuff) if (Object.prototype.hasOwnProperty.call(stuff, type) && stuff[type].rawids) { stuff[type].ids = stuff[type].rawids.length > 0 ? stuff[type].rawids.trim().split(" ") : []; modal(true, createTag("span", {}, [createTag("code", {s: {whiteSpace: "pre", textShadow: "0 0 8px " + highlightColour}}, stuff[type].ids.length.toLocaleString(lang).padStart(12, " ")), createTag("b", {}, " " + type.replace(/-/, " ") + (stuff[type].ids.length == 1 ? "" : "s")), "… "])); GM_setValue(type + "s", stuff[type].rawids); modal(true, "saved.", 1); // free up memory stuff[type].rawids = ""; stuff[type].ids = []; } GM_setValue("collections", collectionsID); modal(true, concat(["<br>", createTag("b", {s: {color: highlightColour}}, "Everything loaded!")]), 1); modal(true, concat(["<hr>", "Collection loaded in highlighter in ", sInt2msStr(Math.floor((Date.now() - collectionLoadingStartDate) / 1000)), ".", "<br>", "You may now enjoy this stuff highlighted in various appropriate places. YAY(^o^;)Y", "<br>"]), 1); } function error(msg) { setTitle(false); modal(true, createTag("pre", {s: {fontWeight: "bolder", backgroundColor: "yellow", color: "red", border: "2px dashed black", boxShadow: "2px 2px 2px grey", width: "fit-content", margin: "1em auto", padding: "1em"}}, createTag("code", {}, msg)), 1).style.setProperty("background-color", "pink"); alert(dialogprefix + msg); modal(true, concat(["You may ", createA("have a look at known issues and/or create a new bug report", GM_info.script.namespace + "/issues/new?labels=" + GM_info.script.name.replace(". ", "_").replace(" ", "-")), " or just ", createA("reload this page", function(event) { self.location.reload(); }), "."]), 1); unlockModal(); } function unlockModal() { modal(true, concat(["☞ You can now review these cute logs, or close it (press “Escape” or click outside). ஜ۩۞۩ஜ"]), 1); document.getElementById(prefix + "Modal").previousSibling.addEventListener("click", closeModal); document.body.addEventListener("keydown", closeModal); } function closeModal(event) { if (event.type == "click" || event.type == "keydown" && event.key == "Escape") { var modalWall = document.getElementById(prefix + "Modal").previousSibling; if (gaugeto) { clearTimeout(gaugeto); gaugeto = null; } modalWall.parentNode.removeChild(modalWall.nextSibling); modalWall.parentNode.removeChild(modalWall); if (event.type == "keydown") { document.body.removeEventListener("keydown", closeModal); } } } var gaugeto; function modal(show, data, newlines, gauge) { var obj = document.getElementById(prefix + "Modal"); if (show && !obj) { coolstuff("div", "50", "100%", "100%", "black", ".6"); obj = coolstuff("div", "55", "600px", "92%", "white"); obj.setAttribute("id", prefix + "Modal"); obj.style.setProperty("padding", "4px"); obj.style.setProperty("overflow", "auto"); obj.style.setProperty("border", "4px solid black"); obj.addEventListener("mouseover", function(event) { this.style.setProperty("border-color", "silver"); }, false); obj.addEventListener("mouseout", function(event) { this.style.setProperty("border-color", "black"); }, false); var gaug = obj.appendChild(document.createElement("div")); gaug.style.setProperty("position", "fixed"); gaug.style.setProperty("left", 0); gaug.style.setProperty("bottom", 0); gaug.style.setProperty("width", 0); gaug.style.setProperty("background", "black"); gaug.appendChild(document.createTextNode("\u00a0")); gaug = gaug.appendChild(document.createElement("div")); gaug.style.setProperty("position", "fixed"); gaug.style.setProperty("left", 0); gaug.style.setProperty("bottom", 0); gaug.style.setProperty("width", "100%"); gaug.style.setProperty("text-align", "center"); gaug.style.setProperty("color", "white"); gaug.style.setProperty("text-shadow", "1px 2px 2px black"); gaug.appendChild(document.createTextNode("\u00a0")); } if (show && obj) { if (gauge) { var percentage = Math.floor(100 * gauge.current / gauge.total); if (percentage) { var gau = obj.firstChild; if (gaugeto || gau.style.getPropertyValue("display") == "none") { clearTimeout(gaugeto); gaugeto = null; gau.style.setProperty("display", "block"); } gau.style.setProperty("width", Math.ceil(self.innerWidth * gauge.current / gauge.total) + "px"); var elapsedSeconds = Math.floor((Date.now() - currentTaskStartDate) / 1000); var totalSeconds = Math.ceil(elapsedSeconds > 0 ? elapsedSeconds * gauge.total / gauge.current : (gauge.total - gauge.current) * MBWSRate / 1000); gau.lastChild.replaceChild(document.createTextNode((gauge.text ? gauge.text + " " : "") + gauge.current.toLocaleString(lang) + " / " + gauge.total.toLocaleString(lang) + " (" + percentage + "%); loading time: elapsed " + sInt2msStr(elapsedSeconds) + " / estimated total " + sInt2msStr(totalSeconds) + ", remaining " + sInt2msStr(totalSeconds - elapsedSeconds)), gau.lastChild.firstChild); setTitle(true, percentage); if (gauge.current >= gauge.total) { gaugeto = setTimeout(function() { if ((obj = document.getElementById(prefix + "Modal")) !== null) { obj.firstChild.style.setProperty("display", "none"); } }, 10000); } } } if (data) { // neither null nor empty string obj.appendChild(typeof data == "string" ? document.createTextNode(data) : data); } if (newlines) { // neither null nor number 0 for (var b = 0; b < newlines; b++) { obj.appendChild(document.createElement("br")); } } if ((data || newlines) && obj.style.getPropertyValue("border-color") == "black") { obj.scrollTop = obj.scrollHeight; } } if (!show && obj) { obj.parentNode.removeChild(obj.previousSibling); obj.parentNode.removeChild(obj); } return obj; function coolstuff(t, z, x, y, b, o) { var truc = document.body.appendChild(document.createElement(t)); truc.style.setProperty("position", "fixed"); truc.style.setProperty("z-index", z); truc.style.setProperty("width", x); var xx = x.match(/^([0-9]+)(px|%)$/); if (xx) { if (xx[2] == "px" && xx[1] >= self.innerWidth) { truc.style.setProperty("width", (self.innerWidth - 8) + "px"); truc.style.setProperty("left", (self.pageXOffset - 4) + "px"); // pageXOffset unfortunately always returns 0 (?!) } else { truc.style.setProperty("left", ((xx[2] == "%" ? 100 : self.innerWidth) - xx[1]) / 2 + xx[2]); } } truc.style.setProperty("height", y); var yy = y.match(/^([0-9]+)(px|%)$/); if (yy) { truc.style.setProperty("top", ((yy[2] == "%" ? 100 : self.innerHeight) - yy[1]) / 4 + yy[2]); } if (b) { truc.style.setProperty("background", b); } if (o) { truc.style.setProperty("opacity", o); } return truc; } } // ############################################################################ // # # // # ACCESSORY TECHNICAL FUNCTIONS # // # # // ############################################################################ function lastLink(href) { if (href) { GM_setValue("lastlink", href); } else { var ll = GM_getValue("lastlink"); if (ll) { GM_deleteValue("lastlink"); modal(true, "Re‐loading page…", 1); setTimeout(function() { self.location.href = ll; }, chrono(MBWSRate)); } else { modal(true, "", 1); error("Sorry, I’m lost. I don’t know what was the link you last clicked."); } } } function createA(text, link, title, stay) { var a = document.createElement("a"); a.appendChild(document.createTextNode(text)); if (link && typeof link == "string") { a.setAttribute("href", link); if (!stay) { a.setAttribute("target", "_blank"); } } else { if (link && typeof link == "function") { a.addEventListener("click", link, false); } a.style.setProperty("cursor", "pointer"); } if (title){ a.setAttribute("title", title); } return a; } function concat(_nodes) { var concats = document.createDocumentFragment(); var nodes = (typeof _nodes != "object" || !_nodes.length) ? [_nodes] : _nodes; for (let n = 0; n < nodes.length; n++) { var ccat = nodes[n]; if (typeof ccat == "string") { var ccatr = ccat.match(/^<(.+)>$/); if (ccatr) { ccat = document.createElement(ccatr[1]); } else { ccat = document.createTextNode(ccat); } } concats.appendChild(ccat); } return concats; } var lastchrono; function chrono(delay) { if (delay) { var del = delay + lastchrono - new Date().getTime(); del = del > 0 ? del : 0; return del; } else { lastchrono = new Date().getTime(); return lastchrono; } } function sInt2msStr(seconds) { var s = seconds; var div = s % (60 * 60); var h = Math.floor(s / (60 * 60)); var m = Math.floor(div / 60); s = Math.ceil(div % 60); return ((h > 0 ? h + ":" : "") + (m > 9 ? m : "0" + m) + ":" + (s > 9 ? s : "0" + s)); } function debug(txt) { if (DEBUG) { console.debug(prefix + "\n" + txt); } } function debugRetry(status) { debug("Error " + status + "\nRetrying (" + retry + "/" + maxRetry + ") in " + retryPause + " ms\nSlowing down, new rate: " + (MBWSRate - slowDownStepAfterRetry) + "+" + slowDownStepAfterRetry + " = " + MBWSRate + " ms"); } function nsr(prefix) { var ns = { xhtml: "http://www.w3.org/1999/xhtml", mb: "http://musicbrainz.org/ns/mmd-2.0#", }; return ns[prefix] || null; }