- // ==UserScript==
- // @name MusicBrainz function library
- // @namespace http://www.jens-bertram.net/userscripts/mbz-lib
- // @description Musicbrainz function library. Requires jQuery to run.
- // @supportURL https://github.com/JensBee/userscripts
- // @icon https://wiki.musicbrainz.org/-/images/3/39/MusicBrainz_Logo_Square_Transparent.png
- // @license MIT
- // @version 0.3beta
- //
- // @grant none
- // ==/UserScript==
- // Function library to work with MusicBrainz pages.
- // Please beware that this library is not meant for public use. It may change
- // between versions in any incompatible way. If you make use of this library you
- // may want to fork it or use a service like greasyfork which is able to point
- // to a specific version of this library.
- MBZ = null;
-
- /**
- * Event callback for re-using library.
- * @lib Library passed in from callback.
- */
- var loader = function(lib) {
- MBZ = lib;
- };
-
- /**
- * Library specification for re-using.
- */
- var thisScript = {
- id: 'mbz-lib',
- version: '0.3beta',
- loader: loader
- };
-
- // trigger load event
- $(window).trigger('MBZLoadingLibrary', thisScript);
-
- // reuse existing library, if already set by callback
- if (MBZ) {
- console.log("Reusing library", MBZ);
- } else {
- // we have to wrap this in the else statement, because GreasyMonkey does not
- // like a return statement in top-level code
-
- MBZ = {
- baseUrl: 'https://musicbrainz.org/',
- impl: {} // concrete implementations, unloaded after initialization
- };
- MBZ.iconUrl = MBZ.baseUrl + 'favicon.ico',
-
- MBZ.impl.Html = function() {
- this.globStyle = null;
-
- /**
- * Add CSS entry to pages <head/>.
- * @param style definition to add
- */
- function init() {
- if ($('head').length == 0) {
- $('body').append($('<head>'));
- }
- this.globStyle = $('head>style');
- if (this.globStyle.length == 0) {
- this.globStyle = $('<style>');
- this.globStyle.attr('type', 'text/css');
- $('head').append(this.globStyle);
- }
- this.globStyle.append(''
- + 'button.mbzButton{'
- + 'cursor:pointer;'
- + 'text-decoration:none;'
- + 'text-shadow:-1px -1px 0 rgba(255,201,97,0.3);'
- + 'font-weight:bold;'
- + 'color:#000;'
- + 'padding:5px 5px 5px 25px;'
- + 'border-radius:5px;'
- + 'border-top:1px solid #736CAE;'
- + 'border-left:1px solid #736CAE;'
- + 'border-bottom:1px solid #FFC961;'
- + 'border-right:1px solid #FFC961;'
- + 'background:#FFE3B0 url("' + MBZ.iconUrl + '") no-repeat 5px center;'
- + '}'
- + 'button.mbzButton:hover{'
- + 'border:1px solid #454074;'
- + 'background-color:#FFD88C;'
- + '}'
- + 'button.mbzButton:disabled{'
- + 'cursor:default;'
- + 'border:1px solid #ccc;'
- + 'background-color:#ccc;'
- + 'color:#5a5a5a;'
- + '}'
- + 'div#mbzDialog{'
- + 'margin:0.5em 0.5em 0.5em 0;'
- + 'padding:0.5em;'
- + 'background-color:#FFE3B0;'
- + 'border-top:1px solid #736CAE;'
- + 'border-left:1px solid #736CAE;'
- + 'border-bottom:1px solid #FFC961;'
- + 'border-right:1px solid #FFC961;'
- + '}'
- );
- };
-
- /**
- * Add some CSS to the global page style.
- * @style CSS to add
- */
- this.addStyle = function(style) {
- this.globStyle.append(style);
- };
-
- // constructor
- init.call(this);
- };
- MBZ.impl.Html.prototype = {
- mbzIcon: '<img src="' + MBZ.iconUrl + '" />',
-
- /**
- * Create a MusicBrainz link.
- * @params[type] type to link to (e.g. release)
- * @params[id] mbid to link to (optional)
- * @params[more] stuff to add after mbid + '/' (optional)
- * @return plain link text
- */
- getLink: function (params) {
- return MBZ.baseUrl + params.type + '/'
- + (params.id ? params.id + '/' : '') + (params.more || '');
- },
-
- /**
- * Create a MusicBrainz link.
- * @params[type] type to link to (e.g. release)
- * @params[id] mbid to link to (optional)
- * @params[more] stuff to add after mbid + '/' (optional)
- * @params[title] link title attribute (optional)
- * @params[text] link text (optional)
- * @params[before] stuff to put before link (optional)
- * @params[after] stuff to put after link (optional)
- * @params[icon] true/false: include MusicBrainz icon (optional,
- * default: true)
- * @return link jQuery object
- */
- getLinkElement: function (params) {
- params.icon = (typeof params.icon !== 'undefined'
- && params.icon == false ? false : true);
- var retEl = $('<div style="display:inline-block;">');
- if (params.before) {
- retEl.append(params.before);
- }
- var linkEl = $('<a>' + (params.icon ? this.mbzIcon : '')
- + (params.text || '') + '</a>');
- linkEl.attr('href', this.getLink({
- type: params.type,
- id: params.id,
- more: params.more
- })).attr('target', '_blank');
- if (params.title) {
- linkEl.attr('title', params.title);
- }
- retEl.append(linkEl);
- if (params.after) {
- retEl.append(params.after);
- }
- return retEl;
- },
-
- getMbzButton: function(caption, title) {
- var btn = $('<button type="button" class="mbzButton">' + caption
- + '</button>');
- if (title) {
- btn.attr('title', title);
- }
- return btn;
- }
- };
-
- /**
- * Utility functions.
- */
- MBZ.impl.Util = function() {};
- MBZ.impl.Util.prototype = {
- /**
- * Convert anything to string.
- * @data object
- */
- asString: function (data) {
- if (data == null) {
- return '';
- }
- switch (typeof data) {
- case 'string':
- return data.trim();
- case 'object':
- return data.toString().trim();
- case 'function':
- return 'function';
- case 'undefined':
- return '';
- default:
- data = data + '';
- return data.trim();
- }
- },
-
- /**
- * Creates http + https url from a given https? url.
- * @url http/https url
- * @return array with given url prefixed with http + https or single url,
- * if not https? protocol
- */
- expandProtocol: function(url) {
- var urls;
- if (url.toLowerCase().startsWith('http')) {
- var urlPath = url.replace(/^https?:\/\//,'');
- urls = ['http://' + urlPath, 'https://' + urlPath];
- } else {
- urls = [url];
- }
- return urls;
- },
-
- /**
- * Creates http + https urls from a given array of https? urls.
- * @urls array of http/https urls
- * @return array with given urls prefixed with http + https
- */
- expandProtocols: function(urls) {
- var newUrls = [];
- var self = this;
- $.each(urls, function(idx, val){
- newUrls = newUrls.concat(self.expandProtocol(val));
- });
- return newUrls;
- },
-
- /**
- * Get the last path segment from a URL.
- */
- getLastPathSegment: function(str) {
- if (!str || typeof str !== 'string' || str.indexOf('/') == -1) {
- return str;
- }
- var seg = str.split('/');
- return seg[seg.length -1];
- },
-
- /**
- * Detect the MusicBrainz page we're on.
- */
- getMbzPageType: function() {
- var type = [];
- if (this.isMbzPage()) {
- var path = window.location.pathname;
- if (path.contains("/artist/")) {
- type.push("artist");
- } else if (path.contains("/recording/")) {
- type.push("recording");
- } else if (path.contains("/release/")) {
- type.push("release");
- } else if (path.contains('/release-group/')) {
- type.push("release-group");
- }
- var lps = this.getLastPathSegment(path);
- // exclude id strings
- if (!lps.match(/^[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+$/)) {
- type.push(lps);
- }
- }
- return type;
- },
-
- /**
- * Convert HH:MM:SS, MM:SS, SS to seconds.
- * http://stackoverflow.com/a/9640417
- * @str string
- * @return seconds extracted from initial string
- */
- hmsToSeconds: function (str) {
- str = MBZ.Util.asString(str);
- if (str.indexOf(':') > -1) {
- var p = str.split(':'), s = 0, m = 1;
-
- while (p.length > 0) {
- s += m * parseInt(p.pop(), 10);
- m *= 60;
- }
-
- return s;
- } else {
- return str;
- }
- },
-
- /**
- * Check, if we're on a musicbrainz page.
- * @return true if so
- */
- isMbzPage: function() {
- if (window.location.hostname.contains('musicbrainz.org')
- || window.location.hostname.contains('mbsandbox.org')) {
- return true;
- }
- return false;
- },
-
- /**
- * Convert milliseconds to HH:MM:SS.ss string.
- * https://coderwall.com/p/wkdefg
- */
- msToHms: function (ms) {
- str = this.asString(ms);
- if (str.match(/^[0-9]+$/)) {
- var milliseconds = parseInt((ms % 1000) / 100)
- , seconds = parseInt((ms / 1000) % 60)
- , minutes = parseInt((ms / (1000 * 60)) % 60)
- , hours = parseInt((ms / (1000 * 60 * 60)) % 24);
-
- hours = (hours < 10) ? "0" + hours : hours;
- minutes = (minutes < 10) ? "0" + minutes : minutes;
- seconds = (seconds < 10) ? "0" + seconds : seconds;
-
- return (hours && hours != '00' ? (hours + ":") : '') + minutes + ":"
- + seconds + (milliseconds ? ("." + milliseconds) : '');
- } else {
- return ms;
- }
- },
-
- /**
- * Remove a trailing slash from a string
- * @str string
- * @return intial string with trailing slash removed
- */
- rmTrSlash: function (str) {
- if(str.substr(-1) == '/') {
- return str.substr(0, str.length - 1);
- }
- return str;
- }
- };
-
- /**
- * Util functions to work with results from MutationObservers.
- */
- MBZ.impl.Util.Mutations = function() {};
- MBZ.impl.Util.Mutations.prototype = {
- /**
- * Checks mutation records if an element with a given tagName was added.
- * If callback function returns true, no further elements will be checked.
- * @mutationRecords mutation records passed by an observer
- * @tName tagname to check for (case is ignored)
- * @cb callback function
- * @scope optionl scope for callback
- * @return if callback returned true, false otherwise
- */
- forAddedTagName: function(mutationRecords, tName, cb, scope) {
- if (!mutationRecords || !cb || !tName || tName.trim().length == 0) {
- return false;
- }
- tName = tName.toLowerCase();
- return mutationRecords.some(function(mutationRecord){
- for (let node of mutationRecord.addedNodes) {
- if (node.tagName && node.tagName.toLowerCase() == tName) {
- var ret;
- if (scope) {
- ret = cb.call(scope, node);
- } else {
- ret = cb(node);
- }
- if (ret == true) {
- return ret;
- }
- }
- };
- });
- }
- };
-
- /**
- * Shared bubble editor functions.
- */
- MBZ.impl.BubbleEditor = function() {};
- MBZ.impl.BubbleEditor.prototype = {
- /**
- * Add an artist credit.
- * Must be called in scope.
- * @bubble bubble element
- * @data String or array with 1-3 elements. [mb-artist name, artist as
- * credited, join phrase]
- * @noAc if true, displaying the autocomplete popup will be disabled
- */
- addArtist: function(data, noAc) {
- if (typeof data === 'string') {
- data = [data];
- }
- if (data && data.length > 0) {
- var rows = this.getCreditRows();
- if (rows.length > 0) {
- var targets = this.getCreditInputs(rows.get(rows.length -1));
- // check, if row is all empty..
- if (targets[0].val() != '' || targets[1].val() != ''
- || targets[2].val() != '') {
- // ..if not, add one row and re-set target
- if (targets[2].val().trim() == '') {
- // at least in track bubble adding a new artist is not possible
- // without a join-phrase - so add one
- targets[2].val(" & ");
- targets[2].trigger('change');
- }
- $(this.getBubble().find('.add-item').get(0)).click();
- rows = this.getCreditRows();
- targets = this.getCreditInputs(rows.get(rows.length -1));
- }
- if (noAc) {
- targets[0].autocomplete({disabled: true});
- }
-
- targets[0].val(data[0]);
-
- if (data.length > 1) {
- targets[1].val(data[1]);
- } else {
- targets[1].val(data[0]);
- }
- if (data.length > 2) {
- targets[2].val(data[2]);
- }
- targets[0].trigger('input');
-
- if (noAc) {
- targets[0].autocomplete({disabled: false});
- }
- }
- }
- },
-
- /**
- * Get all mb-artist credits currently listed in the bubble editor.
- * Must be called in scope.
- * @return array with artist names
- */
- getArtistCredits: function() {
- var rows = this.getCreditRows();
- var artists = [];
-
- if (rows.length > 0) {
- var self = this;
-
- $.each(rows, function() {
- var row = $(this);
- var inputs = self.getCreditInputs(row);
- if (inputs[0]) {
- artists.push(inputs[0].val());
- }
- });
- }
-
- return artists;
- },
-
- /**
- * See Observer.addAppearCb.
- */
- onAppear: function(params) {
- return this._bubble.observer.addAppearCb(params);
- },
-
- /**
- * See Observer.addChangedCb.
- */
- onContentChange: function(params) {
- return this._bubble.observer.addChangedCb(params);
- },
-
- /**
- * Remove a complete artist credit by it's row.
- * @row artists data row
- */
- removeArtist: function(row) {
- if (row) {
- // may be <button/> or <input/> - so check attribute only
- $(row).find('.remove-artist-credit').click();
- }
- },
-
- /**
- * Get a new array with artists removed already present in bubble editor.
- * Checks are done against the mb artist name. Check is done by using
- * all lower case letters.
- * Must be called in scope.
- * @artists Array of artist names
- */
- removePresentArtists: function(artists) {
- var rows = this.getCreditRows();
- var newArtists = [];
-
- var presentArtists = this.getArtistCredits();
-
- if (rows.length > 0) {
- var presentArtists = [];
- var self = this;
-
- $.each(rows, function() {
- var row = $(this);
- var inputs = self.getCreditInputs(row);
- if (inputs[0]) {
- presentArtists.push(inputs[0].val().toLowerCase());
- }
- });
-
- // sort out new ones
- for (let artist of artists) {
- if (presentArtists.indexOf(artist.toLowerCase()) == -1) {
- newArtists.push(artist);
- }
- }
- }
-
- return newArtists;
- },
-
- /**
- * Tries to open the bubble by clicking the given handler.
- * @bubble bubble element
- * @handler handler to click
- */
- tryOpen: function(handler) {
- var bubble = this.getBubble();
- if (bubble && !bubble.is(':visible')) {
- handler.click();
- }
- },
-
- /**
- * Bubble observer class.
- * @instance Bubble class instance.
- * @ids[bubble] Id of bubble element
- * @ids[container] For two-stage loading: container that will contain the
- * bubble (optional)
- */
- Observer: function(instance) {
- var observer = null;
- var disconnected = false;
- var onAppearCb = [];
- var onChangeCb = [];
- var that = instance;
- var stagedLoading = false;
- var noBubble = false;
-
- function mutated(mutationRecords) {
- if (that._bubble.el) {
- // remove observer, if noone is listening
- if (onChangeCb.length == 0) {
- console.debug("Remove bubble observer - noone listening.");
- observer.disconnect();
- disconnected = true;
- } else {
- for (let cbParams of onChangeCb) {
- cbParams.cb(that._bubble.el, mutationRecords);
- }
- }
- } else {
- var bubble = $(that._bubble.id);
- if (bubble && bubble.length ==1) {
- that._bubble.el = bubble;
- if (stagedLoading) {
- // switch to real bubble element from container
- console.debug("StagedLoading: switching observer to bubble",
- bubble);
- observer.disconnect();
- observer.observe(bubble.get(0), {
- childList: true,
- subtree: true
- });
- }
- hasAppeared();
- }
- }
- };
-
- function hasAppeared() {
- // call onAppear callbacks
- while (onAppearCb.length > 0) {
- onAppearCb.pop().cb(that._bubble.el);
- }
- };
-
- function init() {
- var bubble = $(that._bubble.id);
- var e;
- if (bubble && bubble.length ==1) {
- that._bubble.el = bubble;
- e = bubble.get(0);
- hasAppeared();
- } else if (that._bubble.containerId) {
- stagedLoading = true;
- e = $(that._bubble.containerId).get(0);
- }
-
- if (!e) {
- console.debug(that.type,
- "Bubble not found and no container specified. Giving up.");
- noBubble = true;
- } else {
- observer = new MutationObserver(mutated);
- observer.observe(e, {
- childList: true,
- subtree: true
- });
- }
- };
-
- function reAttach() {
- if (disconnected) {
- console.debug("Re-attach bubble observer - new listener.");
- observer.observe(that._bubble.el.get(0), {
- childList: true,
- subtree: true
- });
- }
- };
-
- /**
- * Add a listener to listen to appearance of the bubble. Callback is
- * called directly, if bubble is already present.
- * @cb[cb] callcack function
- * @return true, if added or called immediately, false, if there's no
- * bubble to attach to
- */
- this.addAppearCb = function(cb) {
- if (noBubble) {
- console.debug("Not attaching to event. No bubble.");
- return false;
- }
- if (that._bubble.el) {
- // direct call, bubble already there
- cb.cb(that._bubble.el);
- } else {
- // add to stack
- onAppearCb.push(cb);
- }
- return true;
- };
-
- /**
- * Add a listener to listen to changes to the bubble.
- * @cb[cb] callcack function
- * @return true, if added, false, if there's no bubble to attach to
- */
- this.addChangedCb = function(cb) {
- if (noBubble) {
- console.debug("Not attaching to event. No bubble.");
- return false;
- }
- reAttach();
- onChangeCb.push(cb);
- return false;
- };
-
- // constructor
- init.call(this);
- }
- };
-
- /**
- * Bubble editors base class.
- */
- MBZ.BubbleEditor = {
- /**
- * Differenciate types of bubble editors.
- */
- types: {
- artistCredits: 'ArtistCreditBubble',
- trackArtistCredits: 'TrackArtistCreditBubble'
- }
- };
-
- /**
- * Artists credits bubble.
- */
- MBZ.BubbleEditor.ArtistCredits = function() {
- this.type = MBZ.BubbleEditor.types.artistCredits;
- this._bubble = {
- el: null,
- id: '#artist-credit-bubble',
- containerId: '#release-editor',
- observer: null
- };
-
- /**
- * Get the bubble element.
- */
- this.getBubble = function() {
- return this._bubble.el;
- };
-
- /**
- * Extract the inputs for mb-artist, credited-artist and join-phrase from a
- * single data row.
- * @row data row
- * @return array with input elements for mb-artist, credited-artist and
- * join-phrase from a single data row.
- */
- this.getCreditInputs = function(row) {
- if (!row || (row.length && row.length == 0)) {
- console.debug("Empty row.");
- return [];
- }
- row = $(row);
-
- var rowData = [];
- var el = row.find('input[type="text"]'); // mb-artist
-
- if (el.length == 1) {
- rowData.push(el);
- el = row.next().find('input[type="text"]'); // artist as credited
- if (el.length == 1) {
- rowData.push(el);
- el = row.next().next().find('input[type="text"]'); // join phrase
- if (el.length == 1) {
- rowData.push(el);
- return rowData;
- }
- }
- }
- return [];
- };
-
- /**
- * Get the rows containing inputs for mb-artist, credited-artist and
- * join-phrase from the bubble.
- * @return jQuery object containing each data row. This is for each entry
- * the first row containing the mb-artist name.
- */
- this.getCreditRows = function() {
- if (this._bubble.el) {
- return this._bubble.el.find('tr:has(input.name)');
- } else {
- console.debug("No rows found. Bubble not present.");
- return $();
- }
- };
-
- this._bubble.observer = new this.Observer(this);
- };
-
- /**
- * Track artists credits bubble.
- */
- MBZ.BubbleEditor.TrackArtistCredits = function() {
- this.type = MBZ.BubbleEditor.types.trackArtistCredits;
- this._bubble = {
- el: null,
- id: '#track-ac-bubble',
- observer: null
- };
-
- /**
- * Get the bubble element.
- */
- this.getBubble = function() {
- return this._bubble.el;
- };
-
- /**
- * Get the rows containing inputs for mb-artist, credited-artist and
- * join-phrase from the bubble.
- * @return jQuery object containing each data row
- */
- this.getCreditRows = function() {
- if (this._bubble.el) {
- return this._bubble.el.find('tr:has(td span.artist)');
- } else {
- console.debg("No rows found. Bubble not present.");
- return $();
- }
- };
-
- /**
- * Extract the inputs for mb-artist, credited-artist and join-phrase from a
- * single data row.
- * @row data row
- * @return array with input elements for mb-artist, credited-artist and
- * join-phrase from a single data row.
- */
- this.getCreditInputs = function(row) {
- if (!row) {
- console.debug("Empty row.");
- return [];
- }
-
- var inputs = $(row).find('td input[type="text"]');
- if (inputs.length == 3) {
- return [
- $(inputs.get(0)), // mb-artist
- $(inputs.get(1)), // artist as credited
- $(inputs.get(2)) // join-phrase
- ];
- } else {
- return [];
- }
- };
-
- this._bubble.observer = new this.Observer(this);
- };
-
- /**
- * Release tracklist.
- */
- MBZ.impl.TrackList = function() {
- var observer;
- var id = '#tracklist';
-
- var Observer = function() {
- var observer;
- var onChangeCb = [];
-
- function attach() {
- console.debug("Creating tracklist observer - new listener.");
- observer = new MutationObserver(mutated);
- observer.observe($(id).get(0), {
- childList: true,
- subtree: true
- });
- };
-
- function mutated(mutationRecords) {
- var element = $(id);
- for (cb of onChangeCb) {
- cb.cb(element, mutationRecords);
- }
- };
-
- /**
- * Add a listener to listen to changes to the bubble.
- * @cb[cb] callcack function
- */
- this.addChangedCb = function(cb) {
- if (!observer) {
- attach();
- }
- onChangeCb.push(cb);
- };
- };
-
- this.getList = function() {
- return $(id);
- };
-
- this.onContentChange = function(params) {
- if (!observer) {
- console.debug("Not attaching to event. No tracklist.");
- return false;
- }
- return observer.addChangedCb(params);
- };
-
- if ($(id).length == 1) {
- observer = new Observer();
- }
- };
-
- /**
- * Cover art archive.
- */
- MBZ.impl.CA = function() {};
- MBZ.impl.CA.prototype = {
- baseUrl: 'https://coverartarchive.org/',
- originBaseUrl: 'https://cors-anywhere.herokuapp.com/coverartarchive.org:443/',
-
- /**
- * Create a CoverArtArchive link.
- * @params[type] type to link to (e.g. release)
- * @params[id] mbid to link to (optional)
- * @params[more] stuff to add after mbid (optional)
- */
- getLink: function (params) {
- return this.originBaseUrl + params.type + '/'
- + (params.id ? params.id + '/' : '') + (params.more || '');
- }
- };
-
- /**
- * MusicBrainz web service v2 interface.
- */
- MBZ.impl.WS = function() {};
- MBZ.impl.WS.prototype = {
- _baseUrl: MBZ.baseUrl + 'ws/2/',
- _queue: [],
- _pollFreq: 1100,
- _pollInterval: null,
-
- /**
- * Add to request queue.
- * @params[cb] callback
- * @params[url] request url
- * @params[args] callback function parameters object
- * @params[scope] scope for calling callback function
- */
- _qAdd: function(params) {
- this._queue.push(params);
- if (!this._pollInterval) {
- if (this._queue.length == 1) {
- this._qPoll();
- }
- this._pollInterval = setInterval(this._qPoll, this._pollFreq);
- }
- },
-
- /**
- * Execute queued requests.
- */
- _qPoll: function() {
- if (MBZ.WS._queue.length > 0) {
- var item = MBZ.WS._queue.pop();
- $.getJSON(item.url, function(data) {
- if (item.args) {
- if (item.scope) {
- item.cb.call(item.scope, data, item.args);
- } else {
- item.cb(data, item.args);
- }
- } else {
- if (item.scope) {
- item.cb.call(item.scope, data);
- } else {
- item.cb(data);
- }
- }
- }).fail(function(jqxhr, textStatus, error) {
- var err = textStatus + ', ' + error;
- console.error("Request (" + item.url + ") failed: " + err);
- if (item.scope) {
- item.cb.call(item.scope);
- } else {
- item.cb();
- }
- });
- } else if (MBZ.WS._queue.length == 0 && MBZ.WS._pollInterval) {
- clearInterval(MBZ.WS._pollInterval);
- }
- },
-
- /**
- * Lookup a musicbrainz url relation
- * @params[cb] callback function
- * @params[res] url to lookup
- * @params[rel] relation type
- * @params[scope] scope for callback function
- */
- getUrlRelation: function (params) {
- this._qAdd({
- cb: params.cb,
- url: this._baseUrl + 'url?resource=' + encodeURIComponent(params.res)
- + '&inc=' + params.rel + '-rels',
- scope: params.scope
- });
- },
-
- /**
- * Lookup musicbrainz url relations
- * @params[urls] array of urls to lookup
- * @params[rel] relation type
- * @params[cb] callback function for each response
- * @params[cbInc] callback for each item looked up
- * @params[cbDone] callback to call if all items have been looked up
- * @params[scope] scope for callback functions
- */
- getUrlRelations: function(params) {
- var self = this;
- var count = params.urls.length;
- var current = 0;
- function localCb(data) {
- if (params.scope) {
- params.cb.call(params.scope, data);
- } else {
- params.cb(data);
- }
- if (typeof params.cbInc === 'function') {
- if (params.scope) {
- params.cbInc.call(params.scope);
- } else {
- params.cbInc();
- }
- }
- if (++current == count && typeof params.cbDone === 'function') {
- if (params.scope) {
- params.cbDone.call(params.scope);
- } else {
- params.cbDone();
- }
- }
- }
- $.each(params.urls, function(idx, val) {
- self.getUrlRelation({
- cb: localCb,
- res: val,
- rel: params.rel
- });
- });
- }
- };
-
- /**
- * Library initialization.
- */
- function init() {
- // base
- console.debug("Loading MBZ base classes");
- MBZ.Html = new MBZ.impl.Html();
- MBZ.Util = new MBZ.impl.Util();
- MBZ.Util.Mutations = new MBZ.impl.Util.Mutations();
- MBZ.CA = new MBZ.impl.CA();
- MBZ.WS = new MBZ.impl.WS();
-
- // initialize the following only on MusicBrainz pages
- var pageType = MBZ.Util.getMbzPageType();
- if (pageType.length > 0) {
- // bubble editors
- if (pageType.indexOf("edit") > -1 || pageType.indexOf("add") > -1) {
- // track editor only, if we edit releases
- if (pageType.indexOf("release") > -1) {
- console.debug("Loading MBZ.BubbleEditor.TrackArtistCredits");
- MBZ.BubbleEditor.TrackArtistCredits.prototype =
- new MBZ.impl.BubbleEditor();
- MBZ.BubbleEditor.TrackArtistCredits =
- new MBZ.BubbleEditor.TrackArtistCredits();
- }
-
- // artist editor on artist edit or release types
- if (pageType.indexOf("artist") > -1
- || pageType.indexOf("release") > -1
- || pageType.indexOf("release-group") > -1) {
- console.debug("Loading MBZ.BubbleEditor.ArtistCredits");
- MBZ.BubbleEditor.ArtistCredits.prototype =
- new MBZ.impl.BubbleEditor();
- MBZ.BubbleEditor.ArtistCredits = new MBZ.BubbleEditor.ArtistCredits();
- }
- }
-
- // tracklist is only available on release pages
- if (pageType.indexOf("release") > -1) {
- console.debug("Loading MBZ.TrackList");
- MBZ.TrackList = new MBZ.impl.TrackList();
- }
- }
-
- // release MBZ.impl.* classes to garbage collection
- console.debug("Unloading MBZ.impl.*");
- delete MBZ.impl;
- }
- init();
-
- // Library initialization finished.
- // ============================== On demand classes - created by users =======
-
- /**
- * Release related functions.
- */
- MBZ.Release = function() {
- var form = $('<form method="post" id="' + MBZ.Release._form.baseName + '-'
- + (MBZ.Release._form.count++) + '" target="_blank" action="'
- + MBZ.Release._form.target + '" acceptCharset="UTF-8"></form>');
-
- this.data = {
- annotation: '', // content
- artists: [],
- labels: [],
- mediums: [],
- note: '', // content
- packaging: '', // type
- releases: [],
- title: '', // content
- tracks: [],
- urls: [] // [target, type]
- };
-
- function addField(name, value) {
- name = MBZ.Util.asString(name);
- value = MBZ.Util.asString(value);
- if (name.length > 0 && value.length > 0) {
- form.append($('<input type="hidden" name="' + name + '" value="' + value
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- + '"/>'));
- }
- }
-
- function buildForm(dataSet) {
- if (dataSet.annotation != '') {
- addField('annotation', dataSet.annotation);
- }
-
- if (dataSet.artists.length > 0) {
- $.each(dataSet.artists, function(idx, val) {
- var prefix = 'artist_credit.names.' + (val.idx || idx);
- addField(prefix + '.name', val.cred);
- addField(prefix + '.mbid', val.id);
- addField(prefix + '.artist.name', val.name);
- addField(prefix + '.join_phrase', val.join);
- });
- }
-
- if (dataSet.labels.length > 0) {
- $.each(dataSet.labels, function(idx, val) {
- var prefix = 'labels.' + (val.idx || idx);
- addField(prefix + '.mbid', val.id);
- addField(prefix + '.name', val.name);
- addField(prefix + '.catalog_number', val.catNo);
- });
- }
-
- if (dataSet.note != '') {
- addField('edit_note', dataSet.note);
- }
-
- if (dataSet.releases.length > 0) {
- $.each(dataSet.releases, function(idx, val) {
- var prefix = 'events.' + (val.idx || idx);
- addField(prefix + '.date.year', val.y);
- addField(prefix + '.date.month', val.m);
- addField(prefix + '.date.day', val.d);
- addField(prefix + '.country', val.cc);
- });
- }
-
- $.each(dataSet.mediums, function(idx, val) {
- var prefix = 'mediums.' + (val.idx || idx);
- addField(prefix + '.format', val.fmt);
- addField(prefix + '.name', val.name);
- });
-
- if (dataSet.packaging != '') {
- addField('packaging', dataSet.packaging);
- }
-
- if (dataSet.title != '') {
- addField('name', dataSet.title);
- }
-
- $.each(dataSet.tracks, function(idx, val) {
- var prefix = 'mediums.' + val.med + '.track.' + (val.idx || idx);
- addField(prefix + '.name', val.tit);
- addField(prefix + '.number', val.num);
- addField(prefix + '.recording', val.recId);
- addField(prefix + '.length', val.dur);
-
- if (val.artists) {
- $.each(val.artists, function(aIdx, aVal) {
- var aPrefix = prefix + '.artist_credit.names.' + (aVal.idx || aIdx);
- addField(aPrefix + '.name', aVal.cred);
- addField(aPrefix + '.mbid', aVal.id);
- addField(aPrefix + '.artist.name', aVal.name);
- addField(aPrefix + '.join_phrase', aVal.join);
- });
- }
- });
-
- if (dataSet.urls.length > 0) {
- $.each(dataSet.urls, function(idx, val) {
- addField('urls.' + idx + '.url', val[0]);
- addField('urls.' + idx + '.link_type', val[1]);
- });
- }
- }
-
- /**
- * Submit data to musicbrainz.
- */
- this.submitRelease = function() {
- buildForm(this.data);
- $('body').append(form);
- form.submit();
- };
- };
-
- MBZ.Release._relationCb = function(data) {
- if (!data) {
- return {};
- }
- if (data.relations) {
- var rels = {_res: data.resource};
- $.each(data.relations, function(idx, val) {
- var id = val.release.id;
- var type = val.type;
- if (!rels[id]) {
- rels[id] = [];
- }
- if (rels[id].indexOf(type) == -1) {
- rels[id].push(type);
- }
- });
- return rels;
- }
- };
-
- MBZ.Release._form = {
- baseName: 'mbAddReleaseForm',
- count: 0,
- target: MBZ.baseUrl + 'release/add'
- };
-
- /**
- * Lookup a musicbrainz url relation for 'release' type.
- * @params[cb] callback function
- * @params[res] url to lookup
- * @params[scope] scope for callback function
- */
- MBZ.Release.getUrlRelation = function(params) {
- function innerCb(cbData) {
- if (params.scope) {
- params.cb.call(params.scope, MBZ.Release._relationCb(cbData));
- } else {
- params.cb(MBZ.Release._relationCb(cbData));
- }
- }
- MBZ.WS.getUrlRelation({
- cb: innerCb,
- res: params.res,
- rel: 'release',
- scope: params.scope
- });
- };
-
- /**
- * Lookup musicbrainz url relations for 'release' type.
- * @params[urls] array of urls to lookup
- * @params[cb] callback function for each response
- * @params[cbInc] callback for each item looked up
- * @params[cbDone] callback to call if all items have been looked up
- * @params[scope] scope for callback functions
- */
- MBZ.Release.getUrlRelations = function(params) {
- function innerCb(cbData) {
- if (params.scope) {
- params.cb.call(params.scope, MBZ.Release._relationCb(cbData));
- } else {
- params.cb(MBZ.Release._relationCb(cbData));
- }
- }
- MBZ.WS.getUrlRelations({
- urls: params.urls,
- rel: 'release',
- cb: innerCb,
- cbInc: params.cbInc,
- cbDone: params.cbDone,
- scope: params.scope
- });
- };
-
- /**
- * Insert a link, if a release has MusicBrainz relations.
- * @data key=mbid value=string array: relation types
- * @target target jQuery element to append (optional) or
- * this.mbLinkTarget set in scope
- */
- MBZ.Release.insertMBLink = function(data, target) {
- if (data) {
- var self = this;
- target = target || self.mbLinkTarget;
- if (!target) {
- return;
- }
- $.each(data, function(k, v) {
- if (!k.startsWith('_')) { // skip internal data
- var relLink = MBZ.Html.getLinkElement({
- type: 'release',
- id: k,
- title: "Linked as: " + v.toString(),
- before: ' '
- });
- target.after(relLink);
- var editLink = MBZ.Html.getLinkElement({
- type: 'release',
- id: k,
- more: 'edit',
- text: 'edit',
- title: 'Edit release',
- before: ', ',
- icon: false
- });
- var artLinkTitle = 'set';
- $.ajax({
- url: MBZ.CA.getLink({
- type: 'release',
- id: k,
- more: 'front'
- })
- }).success(function(){
- artLinkTitle = 'edit';
- }).always(function() {
- var artLink = MBZ.Html.getLinkElement({
- type: 'release',
- id: k,
- more: 'cover-art',
- text: artLinkTitle + ' art',
- title: artLinkTitle + ' cover art for release',
- before: ', ',
- icon: false
- });
- relLink.after('<sup> ' + v.length + editLink.html()
- + artLink.html() + '</sup>');
- });
- }
- });
- }
- };
-
- MBZ.Release.prototype = {
- /**
- * Add an artist entry.
- * @params plain artist name as string or object:
- * params[cred] artist name as credited
- * params[id] artists mbid
- * params[idx] position
- * params[join] phrase to join with next artist
- * params[name] artist name
- */
- addArtist: function(params) {
- if (typeof params === 'string') {
- this.data.artists.push({name: params});
- } else {
- this.data.artists.push(params);
- }
- },
-
- /**
- * Add a label entry.
- * @params plain label name as string or object.
- * params[catNo] catalog number
- * params[id] mbid
- * params[idx] position
- * params[name] label name
- */
- addLabel: function(params) {
- if (typeof params === 'string') {
- this.data.labels.push({name: params});
- } else {
- this.data.labels.push(params);
- }
- },
-
- /**
- * Set format of a medium.
- * @params[idx] position
- * @params[fmt] format type name
- * @params[name] name
- */
- addMedium: function(params) {
- this.data.mediums.push(params)
- },
-
- /**
- * Add a release event.
- * @params[y] YYYY
- * @params[m] MM
- * @params[d] DD
- * @params[cc] country code
- * @params[idx] position
- */
- addRelease: function(params) {
- this.data.releases.push(params);
- },
-
- /**
- * Add a track.
- * @params[med] medium number
- * @params[tit] track name
- * @params[idx] track number
- * @params[num] track number (free-form)
- * @params[dur] length in MM:SS or milliseconds
- * @params[recId] mbid of existing recording to associate
- * @params[artists] array of objects:
- * obj[cred] artist name as credited
- * obj[id] artists mbid
- * obj[idx] position
- * obj[join] phrase to join with next artist
- * obj[name] artist name
- */
- addTrack: function(params) {
- this.data.tracks.push(params);
- },
-
- /**
- * @url target url
- * @type musicbrainz url type
- * @return true if value was added
- */
- addUrl: function(url, type) {
- url = MBZ.Util.asString(url);
- type = MBZ.Util.asString(type);
-
- this.data.urls.push([url, type]);
- return true;
- },
-
- /**
- * Dump current data (best viewed in FireBug).
- */
- dump: function() {
- console.log(this.data);
- },
-
- /**
- * @content annotation content
- * @return old value
- */
- setAnnotation: function(content) {
- var old = this.data.annotation;
- this.data.annotation = MBZ.Util.asString(content);
- return old;
- },
-
- /**
- * @content edeting note content
- * @return old value
- */
- setNote: function(content) {
- var old = this.data.note;
- this.data.note = MBZ.Util.asString(content);
- return old;
- },
-
- /**
- * @content packaging type
- * @return old value
- */
- setPackaging: function(type) {
- var old = this.data.packaging;
- this.data.packaging = MBZ.Util.asString(type);
- return old;
- },
-
- /**
- * @name release title
- * @return old value
- */
- setTitle: function(name) {
- var old = this.data.title;
- this.data.title = MBZ.Util.asString(name);
- return old;
- },
- };
-
- $(window).on('MBZLoadingLibrary', function(e, ts){
- if (ts.id == thisScript.id && ts.version == thisScript.version
- && typeof ts.loader === 'function') {
- ts.loader(MBZ);
- }
- });
- }