MusicBrainz function library

Musicbrainz function library. Requires jQuery to run.

目前為 2014-10-13 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/5140/21079/MusicBrainz%20function%20library.js

// ==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.2.1beta
//
// @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 = {};

MBZ.baseUrl = 'https://musicbrainz.org/';
MBZ.iconUrl = MBZ.baseUrl + 'favicon.ico',

MBZ.Html = function() {
  this.globStyle = null;
  this.mbzIcon = '<img src="' + MBZ.iconUrl + '" />';

  /**
    * 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);
  };

  /**
    * 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
    */
  this.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
    */
  this.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;
  };

  this.getMbzButton = function(caption, title) {
    var btn = $('<button type="button" class="mbzButton">' + caption
      + '</button>');
    if (title) {
      btn.attr('title', title);
    }
    return btn;
  };

  init.call(this);
};
MBZ.Html = new MBZ.Html();

MBZ.Util = {
  /**
   * 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;
  },

  getLastPathSegment: function(str) {
    if (!str || typeof str !== 'string' || str.indexOf('/') == -1) {
      return str;
    }
    var seg = str.split('/');
    return seg[seg.length -1];
  },

  /**
   * 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;
    }
  },

  /**
    * Convert milliseconds to HH:MM:SS.ss string.
    * https://coderwall.com/p/wkdefg
    */
  msToHms: function (ms) {
    str = MBZ.Util.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.Util.Mutations = {
  /**
    * 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.BubbleEditor = function() {};
MBZ.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
          $(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);
      } else {
        console.debug("Bubble not found and no container specified. Giving up.");
        noBubble = true;
        return;
      }
      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;
    };

    init.call(this);
  }
};

/**
  * Differenciate types of bubble editors.
  */
MBZ.BubbleEditor.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);
};
MBZ.BubbleEditor.ArtistCredits.prototype = new MBZ.BubbleEditor();
MBZ.BubbleEditor.ArtistCredits = new MBZ.BubbleEditor.ArtistCredits();

/**
  * 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);
};
MBZ.BubbleEditor.TrackArtistCredits.prototype = new MBZ.BubbleEditor();
MBZ.BubbleEditor.TrackArtistCredits = new MBZ.BubbleEditor.TrackArtistCredits();

/**
  * Release tracklist.
  */
MBZ.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();
  }
};
MBZ.TrackList = new MBZ.TrackList();

/**
  * Cover art archive.
  */
MBZ.CA = {
  baseUrl: 'https://coverartarchive.org/',
  // no https here (bad_cert notice)
  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.WS = {
  _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
      });
    });
  }
};

/**
 * 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, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
      + '"/>'));
    }
  }

  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: '&nbsp;'
        });
        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;
  },
};

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址