// ==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 = "
';
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 \n ").concat(variant === 'full' ? 'Import with Harmony' : '', "\n ");
}
// compute HTML of search button
function buildSearchButton(release) {
var parameters = searchParams(release);
var 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();
}
})();