MusicBrainz function library

Musicbrainz function library.

Verzia zo dňa 26.09.2014. Pozri najnovšiu verziu.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greatest.deepsurf.us/scripts/5140/18912/MusicBrainz%20function%20library.js

// ==UserScript==
// @name        MusicBrainz function library
// @namespace   http://www.jens-bertram.net/userscripts/mbz-lib
// @description Musicbrainz function library.
// @require     https://code.jquery.com/jquery-2.1.1.min.js
// @version     0.2.0beta
// @grant       none
// @supportURL  https://github.com/JensBee/userscripts
// @license     MIT
// ==/UserScript==
window.MBZ = {};

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

MBZ.Html = {
  /**
    * Add CSS entry to pages <head/>.
    * @param style definition to add
    */
  _addGlobalStyle: function(css) {
    if ($('head').length == 0) {
      $('body').append($('<head>'));
    }
    var style = $('head>style');
    if (style.length == 0) {
      style = $('<style>');
      style.attr('type', 'text/css');
      $('head').append(style);
    }
    style.append(css);
  },

  _init: function() {
    this._addGlobalStyle(
      '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;}'
    );
  },

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

  mbzIcon: '<img src="' + MBZ.iconUrl + '" />'
};
MBZ.Html._init();

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

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

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

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

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