您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Musicbrainz function library. Requires jQuery to run.
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/5140/22430/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.4beta // // @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.4beta', 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 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; hasAppeared(); } } }; function hasAppeared() { // call onAppear callbacks while (onAppearCb.length > 0) { onAppearCb.pop().cb(that._bubble.el); } // check if someone is listening for changes if (onChangeCb.length == 0) { if (observer) { console.debug("Remove bubble observer - noone listening."); observer.disconnect(); disconnected = true; } else { console.debug("Not attaching bubble observer - noone listening."); } } }; function init() { var bubble = $(that._bubble.id); var e; if (bubble && bubble.length ==1) { that._bubble.el = bubble; e = bubble.get(0); hasAppeared(); } if (!e) { console.debug(that.type, "Bubble not found. 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', 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 $(); } }; /** * Called when bubble has appeared to call the real observer. */ function attachObserver() { this._bubble.observer = new this.Observer(this); } // wait until bubble container appears MBZ.ReleaseEditor.onAppear({ cb: attachObserver, scope: this, selector: this._bubble.id }); }; /** * 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.impl.ReleaseEditor = function() { var observer = new MutationObserver(mutated); var e = $('#release-editor'); var onAppearCb = []; var observing = false; function checkExistance(cbConf) { var result = e.find(cbConf.selector); if (result.length > 0) { if (cbConf.scope) { cbConf.cb.call(cbConf.scope, result); } else { cbConf.cb(result); } return true; } return false; }; function checkObserver() { if (onAppearCb.length > 0) { if (!observing) { observer.observe(e.get(0), { childList: true, subtree: true }); observing = true; console.debug("Attach ReleaseEditor observer - new listener."); } } else if (observing) { observer.disconnect(); observing = false; console.debug("Remove ReleaseEditor observer - noone listening."); } }; function mutated(mutationRecords) { var surviving = []; for (var cbConf of onAppearCb) { if (!checkExistance(cbConf)) { surviving.push(cbConf); } } onAppearCb = surviving; checkObserver(); }; /** * cbConf[selector] element selector to search for * cbConf[cb] callback function * cbConf[scope] optional scope for callback */ this.onAppear = function(cbConf) { if (!checkExistance(cbConf)) { checkObserver(); onAppearCb.push(cbConf); } }; }; /** * 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) { // release editor if (pageType.indexOf("release") > -1 || pageType.indexOf("edit") > -1) { console.debug("Loading MBZ.ReleaseEditor"); MBZ.ReleaseEditor = new MBZ.impl.ReleaseEditor(); } // 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>'); var submitted = false; 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() { if (!submitted) { buildForm(this.data); $('body').append(form); submitted = true; } 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); } }); }