// ==UserScript== // @name Import ElasticStage releases to MusicBrainz // @description One-click importing of releases from elasticstage.com release pages into MusicBrainz // @version 2026.06.28.1 // @author Raman Sinclair // @namespace https://github.com/murdos/musicbrainz-userscripts/ // @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/dist/elasticstage_importer.user.js // @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/dist/elasticstage_importer.user.js // @match https://elasticstage.com/* // @grant none // @run-at document-start // @icon https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/assets/images/Musicbrainz_import_logo.png // ==/UserScript== (function () { 'use strict'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); } function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), Object.defineProperty(e, "prototype", { writable: false }), e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) { t && (r = t); var n = 0, F = function () {}; return { s: F, n: function () { return n >= r.length ? { done: true } : { done: false, value: r[n++] }; }, e: function (r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = true, u = false; return { s: function () { t = t.call(r); }, n: function () { var r = t.next(); return a = r.done, r; }, e: function (r) { u = true, o = r; }, f: function () { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = true, o = false; try { if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = true, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = false, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function (t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = true, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), true), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine(u), _regeneratorDefine(u, o, "Generator"), _regeneratorDefine(u, n, function () { return this; }), _regeneratorDefine(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function () { return { w: i, m: f }; })(); } function _regeneratorDefine(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine = function (e, r, n, t) { function o(r, n) { _regeneratorDefine(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine(e, r, n, t); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } var LogLevel = /*#__PURE__*/function (LogLevel) { LogLevel["DEBUG"] = "debug"; LogLevel["INFO"] = "info"; LogLevel["ERROR"] = "error"; return LogLevel; }({}); var Logger = /*#__PURE__*/function () { function Logger(scriptName) { var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : LogLevel.ERROR; _classCallCheck(this, Logger); _defineProperty(this, "LOG_LEVEL", LogLevel.INFO); _defineProperty(this, "scriptName", void 0); this.scriptName = scriptName; this.LOG_LEVEL = level; } return _createClass(Logger, [{ key: "debug", value: function debug() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } this._log(LogLevel.DEBUG, args); } }, { key: "info", value: function info() { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } this._log(LogLevel.INFO, args); } }, { key: "error", value: function error() { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } this._log(LogLevel.ERROR, args); } }, { key: "setLevel", value: function setLevel(level) { this.LOG_LEVEL = level; } }, { key: "_log", value: function _log(level, args) { if (level < this.LOG_LEVEL) { return; } var logMethod = console.log; switch (level) { case LogLevel.DEBUG: logMethod = console.debug; break; case LogLevel.INFO: logMethod = console.info; break; case LogLevel.ERROR: logMethod = console.error; break; } try { logMethod.apply(this, ["[".concat(this.scriptName, "]")].concat(_toConsumableArray(args))); } catch (_unused) { // do nothing } } }]); }(); // convert HH:MM:SS or MM:SS to milliseconds function hmsToMilliSeconds(str) { if (typeof str == 'undefined' || str === '' || isNaN(Number(str))) return NaN; if (typeof str == 'number') return str; var t = str.split(':'); var s = 0; var m = 1; while (t.length > 0) { s += m * parseInt(t.pop(), 10); m *= 60; } return s * 1000; } // convert ISO8601 duration (limited to hours/minutes/seconds) to milliseconds // format looks like PT1H45M5.789S (note: floats can be used) // https://en.wikipedia.org/wiki/ISO_8601#Durations function ISO8601toMilliSeconds(str) { var regex = /^PT(?:(\d*\.?\d*)H)?(?:(\d*\.?\d*)M)?(?:(\d*\.?\d*)S)?$/; var m = str.replace(',', '.').match(regex); if (!m) return NaN; return (3600 * parseFloat(m[1] || '0') + 60 * parseFloat(m[2] || '0') + parseFloat(m[3] || '0')) * 1000; } // compute HTML of import form function buildFormHTML(parameters) { // Build form var innerHTML = "
"); parameters.forEach(function (parameter) { var value = parameter.value.toString(); innerHTML += ""); }); innerHTML += ''; innerHTML += '
'; return innerHTML; } function luceneEscape(text) { var newText = text.replace(/[-[\]{}()*+?~:\\^!"/]/g, '\\$&'); newText = newText.replace('&&', '&&').replace('||', '||'); return newText; } function appendParameter(parameters, paramName, paramValue) { if (!paramValue) return; parameters.push({ name: paramName, value: paramValue }); } function searchParams(release) { var params = []; var totaltracks = release.discs.reduce(function (acc, _ref) { var tracks = _ref.tracks; return acc + tracks.length; }, 0); var release_artist = ''; for (var i = 0; i < release.artist_credit.length; i++) { var ac = release.artist_credit[i]; if (ac) { release_artist += ac.artist_name; if (typeof ac.joinphrase != 'undefined' && ac.joinphrase != '') { release_artist += ac.joinphrase; } else { if (i != release.artist_credit.length - 1) release_artist += ', '; } } } var query = "artist:(".concat(luceneEscape(release_artist), ")") + " release:(".concat(luceneEscape(release.title), ")") + " tracks:(".concat(totaltracks, ")").concat(release.country ? " country:".concat(release.country) : ''); appendParameter(params, 'query', query); appendParameter(params, 'type', 'release'); appendParameter(params, 'advanced', '1'); return params; } // Try to guess release type using number of tracks, title and total duration (in millisecs) function guessReleaseType(title, num_tracks, duration_ms) { if (num_tracks < 1) return ''; var has_single = !!title.match(/\bsingle\b/i); var has_EP = !!title.match(/\bEP\b/i); if (has_single && has_EP) { has_single = false; has_EP = false; } var perhaps_single = has_single && num_tracks <= 4 || num_tracks <= 2; var perhaps_EP = has_EP || num_tracks > 2 && num_tracks <= 6; var perhaps_album = num_tracks > 8; if (isNaN(duration_ms)) { // no duration, try to guess with title and number of tracks if (perhaps_single && !perhaps_EP && !perhaps_album) return 'single'; if (!perhaps_single && perhaps_EP && !perhaps_album) return 'EP'; if (!perhaps_single && !perhaps_EP && perhaps_album) return 'album'; return ''; } var duration_mn = duration_ms / (60 * 1000); if (perhaps_single && duration_mn >= 1 && duration_mn < 7) return 'single'; if (perhaps_EP && duration_mn > 7 && duration_mn <= 30) return 'EP'; if (perhaps_album && duration_mn > 30) return 'album'; return ''; } function buildArtistCreditsFormParameters(parameters, paramPrefix, artist_credit) { for (var i = 0; i < artist_credit.length; i++) { var ac = artist_credit[i]; if (ac) { appendParameter(parameters, "".concat(paramPrefix, "artist_credit.names.").concat(i, ".name"), ac.credited_name || ''); appendParameter(parameters, "".concat(paramPrefix, "artist_credit.names.").concat(i, ".artist.name"), ac.artist_name); if (ac.mbid) appendParameter(parameters, "".concat(paramPrefix, "artist_credit.names.").concat(i, ".mbid"), ac.mbid); if (typeof ac.joinphrase != 'undefined' && ac.joinphrase != '') { appendParameter(parameters, "".concat(paramPrefix, "artist_credit.names.").concat(i, ".join_phrase"), ac.joinphrase); } } } } // build form POST parameters that MB is expecting function buildFormParameters(release, edit_note) { // Form parameters var parameters = []; appendParameter(parameters, 'name', release.title); // Release Artist credits buildArtistCreditsFormParameters(parameters, '', release.artist_credit); if (release['secondary_types']) { for (var i = 0; i < release.secondary_types.length; i++) { var secondaryType = release.secondary_types[i]; if (secondaryType) { appendParameter(parameters, 'type', secondaryType); } } } if (release.status) appendParameter(parameters, 'status', release.status); if (release.language) appendParameter(parameters, 'language', release.language); if (release.script) appendParameter(parameters, 'script', release.script); if (release.packaging) appendParameter(parameters, 'packaging', release.packaging); // ReleaseGroup if (release.release_group_mbid) appendParameter(parameters, 'release_group', release.release_group_mbid); // Date + country if (release.country) appendParameter(parameters, 'country', release.country); if (!isNaN(release.year || 0) && release.year != 0) { appendParameter(parameters, 'date.year', release.year); } if (!isNaN(release.month || 0) && release.month != 0) { appendParameter(parameters, 'date.month', release.month); } if (!isNaN(release.day || 0) && release.day != 0) { appendParameter(parameters, 'date.day', release.day); } // Barcode if (release.barcode) appendParameter(parameters, 'barcode', release.barcode); // Disambiguation comment if (release.comment) appendParameter(parameters, 'comment', release.comment); // Annotation if (release.annotation) appendParameter(parameters, 'annotation', release.annotation); // Label + catnos if (Array.isArray(release.labels)) { for (var _i = 0; _i < release.labels.length; _i++) { var label = release.labels[_i]; if (label) { appendParameter(parameters, "labels.".concat(_i, ".name"), label.name); if (label.mbid) appendParameter(parameters, "labels.".concat(_i, ".mbid"), label.mbid); if (label.catno && label.catno != 'none') { appendParameter(parameters, "labels.".concat(_i, ".catalog_number"), label.catno); } } } } // URLs if (Array.isArray(release.urls)) { for (var _i2 = 0; _i2 < release.urls.length; _i2++) { var url = release.urls[_i2]; if (url) { appendParameter(parameters, "urls.".concat(_i2, ".url"), url.url); appendParameter(parameters, "urls.".concat(_i2, ".link_type"), url.link_type); } } } // Mediums var total_tracks = 0; var total_tracks_with_duration = 0; var total_duration = 0; for (var _i3 = 0; _i3 < release.discs.length; _i3++) { var disc = release.discs[_i3]; if (disc) { appendParameter(parameters, "mediums.".concat(_i3, ".format"), disc.format); if (disc.title) appendParameter(parameters, "mediums.".concat(_i3, ".name"), disc.title); // Tracks for (var j = 0; j < disc.tracks.length; j++) { var track = disc.tracks[j]; if (track) { total_tracks++; if (track.number) appendParameter(parameters, "mediums.".concat(_i3, ".track.").concat(j, ".number"), track.number); appendParameter(parameters, "mediums.".concat(_i3, ".track.").concat(j, ".name"), track.title); var tracklength = '?:??'; var duration_ms = hmsToMilliSeconds(track.duration); if (!isNaN(duration_ms)) { tracklength = duration_ms.toString(); total_tracks_with_duration++; total_duration += duration_ms; } appendParameter(parameters, "mediums.".concat(_i3, ".track.").concat(j, ".length"), tracklength); // @ts-expect-error TODO: recording is not a property of Track and in no importer scripts a recording is found in a track. Once all scripts are migrated, we need to see if we can remove this line entirely. if (track.recording) appendParameter(parameters, "mediums.".concat(_i3, ".track.").concat(j, ".recording"), track.recording); // eslint-disable-line @typescript-eslint/no-unsafe-argument buildArtistCreditsFormParameters(parameters, "mediums.".concat(_i3, ".track.").concat(j, "."), track.artist_credit); } } } } // Guess release type if not given if (!release.type && release.title && total_tracks == total_tracks_with_duration) { release.type = guessReleaseType(release.title, total_tracks, total_duration); } if (release.type) appendParameter(parameters, 'type', release.type); // Add Edit note parameter if (edit_note) appendParameter(parameters, 'edit_note', edit_note); return parameters; } var styleBlockIconButton = "\n \n"; var styleBlockFullButton = "\n \n"; function buildHarmonyButton(_ref) { var barcode = _ref.barcode, release_url = _ref.release_url, variant = _ref.variant; var searchParams = new URLSearchParams(); if (barcode) { searchParams.set('gtin', barcode); } if (release_url) { searchParams.set('url', encodeURI(release_url)); } searchParams.set('category', 'preferred'); // take Harmony user preferences into account searchParams.set('musicbrainz', ''); // enforce lookup by barcode in MusicBrainz var harmonyURL = "https://harmony.pulsewidth.org.uk/release?".concat(searchParams.toString()); return "\n ".concat(variant === 'full' ? styleBlockFullButton : styleBlockIconButton, "\n \n \"Harmony\n ").concat(variant === 'full' ? 'Import with Harmony' : '', "\n "); } // compute HTML of search button function buildSearchButton(release) { var parameters = searchParams(release); var html = "
"); parameters.forEach(function (parameter) { var value = "".concat(parameter.value); html += ""); }); html += ''; html += '
'; return html; } function buildSearchLink(release) { var parameters = searchParams(release); var url_params = []; parameters.forEach(function (parameter) { var value = "".concat(parameter.value); url_params.push(encodeURI("".concat(parameter.name, "=").concat(value))); }); return "Search in MusicBrainz"); } // Convert a list of artists to a list of artist credits with joinphrases function makeArtistCredits(artists_list) { var artists = artists_list.map(function (item) { return { artist_name: item }; }); if (artists.length > 2) { var last = artists.pop(); if (last) { last.joinphrase = ''; var prev = artists.pop(); if (prev) { prev.joinphrase = ' & '; for (var i = 0; i < artists.length; i++) { var artist = artists[i]; if (artist) { artist.joinphrase = ', '; } } artists.push(prev); artists.push(last); } } } else if (artists.length == 2) { var first = artists[0]; if (first) { first.joinphrase = ' & '; } } var credits = []; // re-split artists if featuring or vs artists.map(function (item) { var c = item.artist_name.replace(/\s*\b(?:feat\.?|ft\.?|featuring)\s+/gi, ' feat. '); c = c.replace(/\s*\(( feat. )([^)]+)\)/g, '$1$2'); c = c.replace(/\s*\b(?:versus|vs\.?)\s+/gi, ' vs. '); c = c.replace(/\s+/g, ' '); var splitted = c.split(/( feat\. | vs\. )/); if (splitted.length === 1) { credits.push(item); // nothing to split } else { var new_items = []; var n = 0; var _iterator = _createForOfIteratorHelper(splitted), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var element = _step.value; if (n && (element === ' feat. ' || element === ' vs. ')) { var prevItem = new_items[n - 1]; if (prevItem) { prevItem.joinphrase = element; } } else { new_items[n++] = { artist_name: element.trim(), joinphrase: '' }; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } var lastItem = new_items[n - 1]; if (lastItem && item.joinphrase) { lastItem.joinphrase = item.joinphrase; } new_items.forEach(function (newit) { return credits.push(newit); }); } }); return credits; } function makeEditNote(release_url, importer_name, format) { var home = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'https://github.com/murdos/musicbrainz-userscripts'; return "Imported from ".concat(release_url).concat(format ? " (".concat(format, ")") : '', " using ").concat(importer_name, " import script from ").concat(home); } function searchUrlFor(type, what) { type = type.replace('-', '_'); var params = ["query=".concat(luceneEscape(what)), "type=".concat(type), 'indexed=1']; return "https://musicbrainz.org/search?".concat(params.join('&')); } function exactSearchUrlFor(type, what) { var limit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 25; type = type.replace('-', '_'); var query = "\"".concat(luceneEscape(what), "\""); var params = ["query=".concat(encodeURIComponent(query)), "type=".concat(type), "limit=".concat(limit), 'method=advanced']; return "https://musicbrainz.org/search?".concat(params.join('&')); } var special_artists = { various_artists: { name: 'Various Artists', mbid: '89ad4ac3-39f7-470e-963a-56509c546377' }, unknown: { name: '[unknown]', mbid: '125ec42a-7229-4250-afc5-e057484327fe' } }; function specialArtist(key, ac) { var joinphrase = ''; if (typeof ac !== 'undefined') { joinphrase = ac.joinphrase || ''; } var specialArtist = special_artists[key]; if (!specialArtist) { throw new Error("Unknown special artist: ".concat(key)); } return { artist_name: specialArtist.name, credited_name: '', joinphrase: joinphrase, mbid: specialArtist.mbid }; } var URL_TYPES = { purchase_for_download: 74, download_for_free: 75, discogs: 76, purchase_for_mail_order: 79, other_databases: 82, stream_for_free: 85, license: 301 }; var MBImport = { buildHarmonyButton: buildHarmonyButton, buildSearchLink: buildSearchLink, buildSearchButton: buildSearchButton, buildFormHTML: buildFormHTML, buildFormParameters: buildFormParameters, makeArtistCredits: makeArtistCredits, guessReleaseType: guessReleaseType, hmsToMilliSeconds: hmsToMilliSeconds, ISO8601toMilliSeconds: ISO8601toMilliSeconds, makeEditNote: makeEditNote, searchUrlFor: searchUrlFor, exactSearchUrlFor: exactSearchUrlFor, URL_TYPES: URL_TYPES, SPECIAL_ARTISTS: special_artists, specialArtist: specialArtist }; function _add_css(css) { document.head.insertAdjacentHTML('beforeend', "")); } function MBImportStyle() { var css_import_button = "\n #mb_buttons {\n display: flex;\n gap: 5px;\n }\n .musicbrainz_import button {\n margin: 0 !important;\n border-radius:5px;\n display: flex;\n justify-content: center;\n align-items: center;\n cursor:pointer;\n font-family:Arial;\n font-size:12px !important;\n padding:3px 6px;\n text-decoration:none;\n border: 1px solid rgba(180,180,180,0.8) !important;\n background-color: rgba(240,240,240,0.8) !important;\n color: #334 !important;\n height: 26px ;\n }\n .musicbrainz_import button:hover {\n background-color: rgba(250,250,250,0.9) !important;\n }\n .musicbrainz_import button:active {\n background-color: rgba(170,170,170,0.8) !important;\n }\n .musicbrainz_import button img {\n vertical-align: middle !important;\n margin-right: 4px !important;\n height: 16px;\n }\n img[src*=\"musicbrainz.org\"] {\n display: inline-block;\n }\n "; _add_css(css_import_button); } /** * Subscribe to Single Page Application (SPA) navigation events. * Uses pushState/replaceState interception when possible; falls back to URL polling in sandboxed environments * (e.g. Firefox/Greasemonkey) where the page uses a different history object. * * @param onNavigate - Callback function to execute when navigation occurs * @param delay - Delay in milliseconds before calling onNavigate (default: 200ms) * @param pollInterval - If set, polls location.href for changes; use when pushState interception doesn't work (default: 400ms, 0 to disable) * @returns Cleanup function to unsubscribe from navigation events */ function subscribeToSPANavigation(_ref) { var onNavigate = _ref.onNavigate, _ref$delay = _ref.delay, delay = _ref$delay === void 0 ? 200 : _ref$delay, _ref$pollInterval = _ref.pollInterval, pollInterval = _ref$pollInterval === void 0 ? 400 : _ref$pollInterval; var currentUrl = window.location.href; var originalPushState = history.pushState.bind(history); var originalReplaceState = history.replaceState.bind(history); var scheduleOnNavigate = function scheduleOnNavigate() { var newUrl = window.location.href; if (newUrl !== currentUrl) { currentUrl = newUrl; setTimeout(function () { void onNavigate(); }, delay); } }; var pushStatePatched = false; var replaceStatePatched = false; try { history.pushState = function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } originalPushState.apply(history, args); scheduleOnNavigate(); }; pushStatePatched = true; } catch (_unused) { // pushState is read-only in some sandboxed environments } try { history.replaceState = function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } originalReplaceState.apply(history, args); scheduleOnNavigate(); }; replaceStatePatched = true; } catch (_unused2) { // replaceState is read-only in some sandboxed environments } var pollTimer; if (pollInterval > 0) { pollTimer = setInterval(scheduleOnNavigate, pollInterval); } var popstateHandler = function popstateHandler() { currentUrl = window.location.href; setTimeout(function () { void onNavigate(); }, delay); }; window.addEventListener('popstate', popstateHandler); return function () { if (pollTimer) clearInterval(pollTimer); if (pushStatePatched) history.pushState = originalPushState; if (replaceStatePatched) history.replaceState = originalReplaceState; window.removeEventListener('popstate', popstateHandler); }; } /** * elasticstage.com is a Vue 3 single-page app. The release page fetches the * release group's releases itself and keeps them in the Vue component state, so * instead of issuing our own API requests we read the data straight out of the * mounted components. This requires running in page context (`@grant none`) so * that the `__vueParentComponent` expando property added by Vue is visible. */ /** Minimal shape of a mounted Vue 3 internal component instance. */ /** The component that renders the list of purchasable formats for a release group. */ var RELEASE_DETAILS_COMPONENT = 'ReleaseDetails'; /** Does this value look like the array of releases exposed by the page? */ function isReleaseArray(value) { if (!Array.isArray(value) || value.length === 0) { return false; } var _ref = value, _ref2 = _slicedToArray(_ref, 1), first = _ref2[0]; return _typeof(first) === 'object' && first !== null && 'ean' in first && 'tracks' in first && 'release_type' in first; } /** * Walk the mounted Vue components and return the release list held in state. * Prefers the dedicated `ReleaseDetails` component but falls back to any * component prop that looks like the release array, in case elasticstage * renames or restructures its components. */ function getReleasesFromVueState() { var fallback = null; var _iterator = _createForOfIteratorHelper(document.querySelectorAll('*')), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _component$type$name, _component$type, _component$type2; var el = _step.value; var component = el.__vueParentComponent; var props = component === null || component === void 0 ? void 0 : component.props; if (!props) { continue; } var name = (_component$type$name = (_component$type = component.type) === null || _component$type === void 0 ? void 0 : _component$type.name) !== null && _component$type$name !== void 0 ? _component$type$name : (_component$type2 = component.type) === null || _component$type2 === void 0 ? void 0 : _component$type2.__name; if (name === RELEASE_DETAILS_COMPONENT && isReleaseArray(props['releases'])) { return cloneReleases(props['releases']); } if (!fallback) { for (var _i = 0, _Object$keys = Object.keys(props); _i < _Object$keys.length; _i++) { var key = _Object$keys[_i]; var value = void 0; try { value = props[key]; } catch (_unused) { continue; } if (isReleaseArray(value)) { fallback = value; break; } } } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return fallback ? cloneReleases(fallback) : []; } /** * Detach the data from Vue's reactive proxies, returning plain serialisable * objects. The release data is plain JSON, so a structured clone is sufficient. */ function cloneReleases(releases) { try { return JSON.parse(JSON.stringify(releases)); } catch (_unused2) { return releases; } } var sleep = function sleep(ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); }; /** * Poll for the release data, which is populated asynchronously after the page * (or an SPA navigation) finishes fetching it. */ function waitForReleases() { return _waitForReleases.apply(this, arguments); } function _waitForReleases() { _waitForReleases = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var maxAttempts, intervalMs, attempt, releases, _args = arguments; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: maxAttempts = _args.length > 0 && _args[0] !== undefined ? _args[0] : 40; intervalMs = _args.length > 1 && _args[1] !== undefined ? _args[1] : 250; attempt = 0; case 1: if (!(attempt < maxAttempts)) { _context.n = 4; break; } releases = getReleasesFromVueState(); if (!(releases.length > 0)) { _context.n = 2; break; } return _context.a(2, releases); case 2: _context.n = 3; return sleep(intervalMs); case 3: attempt++; _context.n = 1; break; case 4: return _context.a(2, []); } }, _callee); })); return _waitForReleases.apply(this, arguments); } var LOGGER = new Logger('elasticstage_importer', LogLevel.INFO); var MB_IMPORT_CONTAINER_ID = 'mb_elasticstage_import'; var MB_STYLE_ID = 'mb_elasticstage_style'; var MB_MINIMIZED_CLASS = 'mb-es-minimized'; var MB_MINIMIZED_STORAGE_KEY = 'mb_elasticstage_minimized'; var MB_LOGO_URL = 'https://raw.githubusercontent.com/metabrainz/design-system/master/brand/logos/MusicBrainz/SVG/MusicBrainz_logo_icon.svg'; function isMinimizedPreferred() { try { return window.localStorage.getItem(MB_MINIMIZED_STORAGE_KEY) === '1'; } catch (_unused) { return false; } } function saveMinimizedPreference(minimized) { try { window.localStorage.setItem(MB_MINIMIZED_STORAGE_KEY, minimized ? '1' : '0'); } catch (_unused2) { // localStorage unavailable; preference simply won't persist } } /** Detect a release page, e.g. /{artist}/releases/{release}. */ function isReleasePage() { return /^\/[^/]+\/releases\/[^/]+/.test(window.location.pathname); } /** Remove any previously inserted import UI to avoid duplicates on SPA navigation. */ function cleanup() { var _document$getElementB; (_document$getElementB = document.getElementById(MB_IMPORT_CONTAINER_ID)) === null || _document$getElementB === void 0 || _document$getElementB.remove(); } /** Map an elasticstage medium string to a MusicBrainz medium format. */ function mapMediumFormat(medium) { switch (medium.trim().toLowerCase()) { case 'cd': return 'CD'; case 'vinyl': return 'Vinyl'; case 'cassette': return 'Cassette'; default: return medium; } } /** Map an elasticstage format string to a MusicBrainz primary release type. */ function mapPrimaryType(format) { switch (format.trim().toLowerCase()) { case 'album': return 'album'; case 'single': return 'single'; case 'ep': return 'EP'; default: return ''; } } /** Normalise the elasticstage artist fields into a flat list of artist names. */ function normalizeArtists(primary, additional) { var names = []; if (primary) { names.push(primary); } var _iterator = _createForOfIteratorHelper(additional), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var entry = _step.value; if (typeof entry === 'string') { if (entry) names.push(entry); } else if (entry && _typeof(entry) === 'object') { var _ref, _record$name; var record = entry; var name = (_ref = (_record$name = record['name']) !== null && _record$name !== void 0 ? _record$name : record['primary_artist']) !== null && _ref !== void 0 ? _ref : record['artist']; if (typeof name === 'string' && name) { names.push(name); } } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return names; } function buildTrackTitle(title, subtitle) { var trimmedSubtitle = subtitle === null || subtitle === void 0 ? void 0 : subtitle.trim(); if (trimmedSubtitle) { return "".concat(title, " (").concat(trimmedSubtitle, ")"); } return title; } function buildReleaseInfo(release_url, esRelease) { var _esRelease$release_da, _esRelease$release_da2; var releaseDate = (_esRelease$release_da = (_esRelease$release_da2 = esRelease.release_date.split('T')[0]) === null || _esRelease$release_da2 === void 0 ? void 0 : _esRelease$release_da2.split('-')) !== null && _esRelease$release_da !== void 0 ? _esRelease$release_da : []; var medium = esRelease.release_type.product_type.medium; var mediumFormat = mapMediumFormat(medium); var isVinyl = medium.trim().toLowerCase() === 'vinyl'; var mbrelease = { artist_credit: [], title: esRelease.title, year: parseInt(releaseDate[0] || '0'), month: parseInt(releaseDate[1] || '0'), day: parseInt(releaseDate[2] || '0'), format: mediumFormat, country: 'XW', status: 'official', type: mapPrimaryType(esRelease.release_type.format || ''), urls: [], labels: [], barcode: esRelease.ean, discs: [] }; mbrelease.artist_credit = MBImport.makeArtistCredits(normalizeArtists(esRelease.primary_artist, esRelease.additional_artists)); mbrelease.urls.push({ url: release_url, link_type: MBImport.URL_TYPES.purchase_for_mail_order }); if (esRelease.label) { var label = { name: esRelease.label }; // elasticstage often reuses the EAN as the catalog number; only keep a // genuine catalog number to avoid polluting MB with the barcode. if (esRelease.catalog_no && esRelease.catalog_no !== esRelease.ean) { label.catno = esRelease.catalog_no; } mbrelease.labels.push(label); } var mbtracks = []; var sideCounters = {}; var _iterator2 = _createForOfIteratorHelper(esRelease.tracks), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var sideGroup = _step2.value; var _iterator3 = _createForOfIteratorHelper(sideGroup), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var esTrack = _step3.value; var artists = normalizeArtists(esTrack.primary_artist, esTrack.additional_artists); var track = { artist_credit: MBImport.makeArtistCredits(artists), title: buildTrackTitle(esTrack.title, esTrack.subtitle), duration: Math.round(esTrack.duration * 1000) }; if (isVinyl) { var _sideCounters$side; var side = esTrack.side || 1; sideCounters[side] = ((_sideCounters$side = sideCounters[side]) !== null && _sideCounters$side !== void 0 ? _sideCounters$side : 0) + 1; track.number = "".concat(String.fromCharCode(64 + side)).concat(sideCounters[side]); } mbtracks.push(track); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } mbrelease.discs.push({ tracks: mbtracks, format: mediumFormat }); return mbrelease; } function buildStyles() { if (document.getElementById(MB_STYLE_ID)) { return; } var style = document.createElement('style'); style.id = MB_STYLE_ID; style.textContent = "\n #".concat(MB_IMPORT_CONTAINER_ID, " {\n position: fixed;\n left: 16px;\n bottom: 16px;\n z-index: 2147483646;\n max-width: 360px;\n max-height: 70vh;\n overflow-y: auto;\n background: rgba(255, 255, 255, 0.97);\n color: #222;\n border: 1px solid rgba(120, 120, 120, 0.6);\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);\n padding: 10px 12px;\n font-family: Arial, sans-serif;\n font-size: 12px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-header {\n display: flex;\n align-items: center;\n gap: 6px;\n font-weight: bold;\n margin-bottom: 8px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-logo {\n flex: none;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-minimize {\n margin-left: auto;\n cursor: pointer;\n border: none;\n background: transparent;\n color: #555;\n font-size: 16px;\n line-height: 1;\n padding: 0 4px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-minimize:hover {\n color: #000;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, ".mb-es-minimized {\n max-width: none;\n width: 44px;\n height: 44px;\n padding: 0;\n overflow: hidden;\n cursor: pointer;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, ".mb-es-minimized .mb-es-header {\n margin-bottom: 0;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, ".mb-es-minimized .mb-es-title,\n #").concat(MB_IMPORT_CONTAINER_ID, ".mb-es-minimized .mb-es-minimize,\n #").concat(MB_IMPORT_CONTAINER_ID, ".mb-es-minimized .mb-es-release {\n display: none;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-release {\n border-top: 1px solid rgba(120, 120, 120, 0.25);\n padding-top: 8px;\n margin-top: 8px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-release:first-of-type {\n border-top: none;\n padding-top: 0;\n margin-top: 0;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-release-title {\n font-weight: bold;\n margin-bottom: 2px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-release-meta {\n color: #555;\n margin-bottom: 6px;\n }\n #").concat(MB_IMPORT_CONTAINER_ID, " .mb-es-buttons {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n align-items: center;\n }\n "); document.head.appendChild(style); } function buildReleaseBlock(esRelease, mbrelease, release_url) { var _mbrelease$discs$0$tr, _mbrelease$discs$; var block = document.createElement('div'); block.className = 'mb-es-release'; var title = document.createElement('div'); title.className = 'mb-es-release-title'; title.textContent = esRelease.release_type.description || esRelease.release_type.product_type.medium; block.appendChild(title); var metaLine = document.createElement('div'); metaLine.className = 'mb-es-release-meta'; var metaParts = ["".concat((_mbrelease$discs$0$tr = (_mbrelease$discs$ = mbrelease.discs[0]) === null || _mbrelease$discs$ === void 0 ? void 0 : _mbrelease$discs$.tracks.length) !== null && _mbrelease$discs$0$tr !== void 0 ? _mbrelease$discs$0$tr : 0, " tracks")]; if (esRelease.ean) metaParts.push("Barcode: ".concat(esRelease.ean)); if (esRelease.is_limited_edition) metaParts.push('Limited edition'); metaLine.textContent = metaParts.join(' ยท '); block.appendChild(metaLine); var editNote = MBImport.makeEditNote(release_url, 'ElasticStage', esRelease.release_type.description); var parameters = MBImport.buildFormParameters(mbrelease, editNote); var buttons = document.createElement('div'); buttons.className = 'mb-es-buttons'; buttons.innerHTML = MBImport.buildFormHTML(parameters) + MBImport.buildSearchButton(mbrelease); block.appendChild(buttons); return block; } function insertMBButtons(esReleases, release_url) { var _header$querySelector; if (esReleases.length === 0) { LOGGER.error('No releases found to import'); return; } buildStyles(); var container = document.createElement('div'); container.id = MB_IMPORT_CONTAINER_ID; var header = document.createElement('div'); header.className = 'mb-es-header'; header.innerHTML = "\n \n Import to MusicBrainz\n \n "); container.appendChild(header); var setMinimized = function setMinimized(minimized) { container.classList.toggle(MB_MINIMIZED_CLASS, minimized); container.title = minimized ? 'Expand MusicBrainz import' : ''; saveMinimizedPreference(minimized); }; (_header$querySelector = header.querySelector('.mb-es-minimize')) === null || _header$querySelector === void 0 || _header$querySelector.addEventListener('click', function (event) { event.stopPropagation(); setMinimized(true); }); // When collapsed to an icon, a click anywhere on it expands it again. container.addEventListener('click', function () { if (container.classList.contains(MB_MINIMIZED_CLASS)) { setMinimized(false); } }); var _iterator4 = _createForOfIteratorHelper(esReleases), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var esRelease = _step4.value; var mbrelease = buildReleaseInfo(release_url, esRelease); container.appendChild(buildReleaseBlock(esRelease, mbrelease, release_url)); } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } if (isMinimizedPreferred()) { container.classList.add(MB_MINIMIZED_CLASS); container.title = 'Expand MusicBrainz import'; } document.body.appendChild(container); } function processReleasePage() { return _processReleasePage.apply(this, arguments); } function _processReleasePage() { _processReleasePage = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var releases, release_url, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: cleanup(); if (isReleasePage()) { _context.n = 1; break; } return _context.a(2); case 1: _context.p = 1; _context.n = 2; return waitForReleases(); case 2: releases = _context.v; if (isReleasePage()) { _context.n = 3; break; } return _context.a(2); case 3: if (!(releases.length === 0)) { _context.n = 4; break; } LOGGER.error('Could not find release data in the page state'); return _context.a(2); case 4: release_url = window.location.href.replace(/[?#].*$/, ''); insertMBButtons(releases, release_url); _context.n = 6; break; case 5: _context.p = 5; _t = _context.v; LOGGER.error('Error processing release page:', _t); case 6: return _context.a(2); } }, _callee, null, [[1, 5]]); })); return _processReleasePage.apply(this, arguments); } function init() { MBImportStyle(); setTimeout(function () { void processReleasePage(); }, 1000); } subscribeToSPANavigation({ onNavigate: function onNavigate() { return processReleasePage(); } }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();