diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b08a742..888fbfb7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -64,6 +64,19 @@ v0.15.0 (UNRELEASED) The methods are still not implemented, but now the commands are accepted as valid. +**HTTP frontend** + +- Fix too broad truthness test that caused :class:`~mopidy.models.TlTrack` + objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the + ``tlid`` field. (Fixes: :issue:`501`) + +- Upgrade Mopidy.js dependencies. + +**Extension support** + +- :class:`~mopidy.config.Secret` is now deserialized to unicode strings instead + of bytestrings. This may require modifications to extensions. + v0.14.2 (2013-07-01) ==================== diff --git a/docs/ext/index.rst b/docs/ext/index.rst index a9857012..736f2fb6 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -46,6 +46,22 @@ Issues: https://github.com/dz0ny/mopidy-beets/issues +Mopidy-GMusic +------------- + +Provides a backend for playing music from `Google Play Music +`_. + +Author: + Ronald Hecht +PyPI: + `Mopidy-GMusic `_ +GitHub: + `hechtus/mopidy-gmusic `_ +Issues: + https://github.com/hechtus/mopidy-gmusic/issues + + Mopidy-NAD ---------- @@ -61,6 +77,22 @@ Issues: https://github.com/mopidy/mopidy/issues +Mopidy-SomaFM +------------- + +Provides a backend for playing music from the `SomaFM `_ +service. + +Author: + Alexandre Petitjean +PyPI: + `Mopidy-SomaFM `_ +GitHub: + `AlexandrePTJ/mopidy-somafm `_ +Issues: + https://github.com/AlexandrePTJ/mopidy-somafm/issues + + Mopidy-SoundCloud ----------------- diff --git a/js/lib/bane-0.4.0.js b/js/lib/bane-1.0.0.js similarity index 98% rename from js/lib/bane-0.4.0.js rename to js/lib/bane-1.0.0.js index a1da6efa..8051764d 100644 --- a/js/lib/bane-0.4.0.js +++ b/js/lib/bane-1.0.0.js @@ -3,10 +3,10 @@ * * https://github.com/busterjs/bane * - * @version 0.4.0 + * @version 1.0.0 */ -((typeof define === "function" && define.amd && function (m) { define(m); }) || +((typeof define === "function" && define.amd && function (m) { define("bane", m); }) || (typeof module === "object" && function (m) { module.exports = m(); }) || function (m) { this.bane = m(); } )(function () { @@ -152,7 +152,7 @@ notifyListener(event, toNotify[i], args); } - toNotify = listeners(this, event).slice() + toNotify = listeners(this, event).slice(); args = slice.call(arguments, 1); for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); diff --git a/js/lib/when-2.0.0.js b/js/lib/when-2.4.0.js similarity index 61% rename from js/lib/when-2.0.0.js rename to js/lib/when-2.4.0.js index 78249532..aa386275 100644 --- a/js/lib/when-2.0.0.js +++ b/js/lib/when-2.4.0.js @@ -9,27 +9,30 @@ * * @author Brian Cavalier * @author John Hann - * @version 2.0.0 + * @version 2.4.0 */ -(function(define) { 'use strict'; -define(function () { +(function(define, global) { 'use strict'; +define(function (require) { // Public API - when.defer = defer; // Create a deferred + when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair when.join = join; // Join 2 or more promises when.all = all; // Resolve a list of promises when.map = map; // Array.map() for promises when.reduce = reduce; // Array.reduce() for promises + when.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. @@ -57,13 +60,35 @@ define(function () { * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state * @name Promise */ - function Promise(then) { - this.then = then; + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; } Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -84,9 +109,7 @@ define(function () { * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { - var self = this; - - return this.then(injectHandler, injectHandler).yield(self); + return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); @@ -107,6 +130,16 @@ define(function () { }); }, + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list @@ -162,13 +195,16 @@ define(function () { } /** - * Creates a new Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise - * only has then. + * has then plus extended promise API. * * @return {{ * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, @@ -216,12 +252,26 @@ define(function () { /** * Creates a new promise whose fate is determined by resolver. - * @private (for now) * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { - var value, handlers = []; + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; // Call the provider resolver to seal the promise's fate try { @@ -231,29 +281,34 @@ define(function () { } // Return the promise - return new Promise(then); + return self; /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify */ - function then(onFulfilled, onRejected, onProgress) { - return promise(function(resolve, reject, notify) { - handlers - // Call handlers later, after resolution - ? handlers.push(function(value) { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }) - // Call handlers soon, but not in the current stack - : enqueue(function() { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }); - }); + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); } /** @@ -262,14 +317,17 @@ define(function () { * @param {*|Promise} val resolution value */ function promiseResolve(val) { - if(!handlers) { + if(!consumers) { return; } value = coerce(val); - scheduleHandlers(handlers, value); + scheduleConsumers(consumers, value); + consumers = undef; - handlers = undef; + if(status) { + updateStatus(value, status); + } } /** @@ -285,27 +343,90 @@ define(function () { * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { - if(handlers) { - scheduleHandlers(handlers, progressing(update)); + if(consumers) { + scheduleConsumers(consumers, progressed(update)); } } } + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce - * @returns {Promise} Guaranteed to return a trusted Promise. If x + * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { - if(x instanceof Promise) { + if (x instanceof Promise) { return x; - } else if (x !== Object(x)) { + } + + if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } @@ -332,61 +453,34 @@ define(function () { } /** - * Create an already-fulfilled promise for the supplied value - * @private + * Proxy for a near, fulfilled value * @param {*} value - * @return {Promise} fulfilled promise + * @constructor */ - function fulfilled(value) { - var self = new Promise(function (onFulfilled) { - try { - return typeof onFulfilled == 'function' - ? coerce(onFulfilled(value)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearFulfilledProxy(value) { + this.value = value; } + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + /** - * Create an already-rejected promise with the supplied rejection reason. - * @private + * Proxy for a near rejection * @param {*} reason - * @return {Promise} rejected promise + * @constructor */ - function rejected(reason) { - var self = new Promise(function (_, onRejected) { - try { - return typeof onRejected == 'function' - ? coerce(onRejected(reason)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearRejectedProxy(reason) { + this.reason = reason; } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressing(update) { - var self = new Promise(function (_, __, onProgress) { - try { - return typeof onProgress == 'function' - ? progressing(onProgress(update)) : self; - } catch (e) { - return progressing(e); - } - }); - - return self; - } + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; /** * Schedule a task that will process a list of handlers @@ -395,7 +489,7 @@ define(function () { * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ - function scheduleHandlers(handlers, value) { + function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { @@ -404,14 +498,23 @@ define(function () { }); } + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + /** - * Determines if promiseOrValue is a promise or not - * - * @param {*} promiseOrValue anything - * @returns {boolean} true if promiseOrValue is a {@link Promise} + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like */ - function isPromise(promiseOrValue) { - return promiseOrValue && typeof promiseOrValue.then === 'function'; + function isPromiseLike(x) { + return x && typeof x.then === 'function'; } /** @@ -423,17 +526,15 @@ define(function () { * @param {Array} promisesOrValues array of anything, may contain a mix * of promises and values * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to an array of howMany values that * resolved first, or will reject with an array of * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - checkCallbacks(2, arguments); - return when(promisesOrValues, function(promisesOrValues) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); @@ -457,7 +558,7 @@ define(function () { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; reject(reasons); } }; @@ -466,7 +567,7 @@ define(function () { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; resolve(values); } }; @@ -496,9 +597,9 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ @@ -519,14 +620,13 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - checkCallbacks(1, arguments); - return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** @@ -535,28 +635,49 @@ define(function () { * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { - return map(arguments, identity); + return _map(arguments, identity); } /** - * Traditional map function, similar to `Array.prototype.map()`, but allows - * input to contain {@link Promise}s and/or values, and mapFunc may return - * either a value or a {@link Promise} - * - * @param {Array|Promise} array array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function} mapFunc mapping function mapFunc(value) which may return - * either a {@link Promise} or value - * @returns {Promise} a {@link Promise} that will resolve to an array containing - * the mapped output values. + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. */ function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { return when(array, function(array) { - return promise(resolveMap); + return _promise(resolveMap); function resolveMap(resolve, reject, notify) { - var results, len, toResolve, resolveOne, i; + var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. @@ -565,27 +686,28 @@ define(function () { if(!toResolve) { resolve(results); - } else { + return; + } - resolveOne = function(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + notify(mapped); + + if(!--toResolve) { + resolve(results); + } + }, reject); + } } }); } @@ -625,12 +747,46 @@ define(function () { }); } + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + // - // Utilities, etc. + // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, - timeout, funcProto, call, arrayProto, undef; + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, undef; + + cjsRequire = require; // // Shared handler queue processing @@ -648,20 +804,13 @@ define(function () { */ function enqueue(task) { if(handlerQueue.push(task) === 1) { - scheduleDrainQueue(); + nextTick(drainQueue); } } /** - * Schedule the queue to be drained in the next tick. - */ - function scheduleDrainQueue() { - nextTick(drainQueue); - } - - /** - * Drain the handler queue entirely or partially, being careful to allow - * the queue to be extended while it is being processed, and to continue + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { @@ -674,20 +823,36 @@ define(function () { handlerQueue = []; } - // - // Capture function and array utils - // - /*global setImmediate:true*/ + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; - // capture setTimeout to avoid being caught by fake timers used in time based tests - timeout = setTimeout; - nextTick = typeof setImmediate === 'function' - ? typeof window === 'undefined' - ? setImmediate - : setImmediate.bind(window) - : typeof process === 'object' - ? process.nextTick - : function(task) { timeout(task, 0); }; + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Prefer setImmediate or MessageChannel, cascade to node, + // vertx and finally setTimeout + /*global setImmediate,MessageChannel,process*/ + if (typeof setImmediate === 'function') { + nextTick = setImmediate.bind(global); + } else if(typeof MessageChannel !== 'undefined') { + var channel = new MessageChannel(); + channel.port1.onmessage = drainQueue; + nextTick = function() { channel.port2.postMessage(0); }; + } else if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // // Safe function calls funcProto = Function.prototype; @@ -748,40 +913,10 @@ define(function () { return reduced; }; - // - // Utility functions - // - - /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. - */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } - } - - function noop() {} - function identity(x) { return x; } return when; }); -})( - typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } -); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); diff --git a/js/package.json b/js/package.json index 1623e3f8..5b8e46d8 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "mopidy", - "version": "0.1.0", + "version": "0.1.1", "description": "Client lib for controlling a Mopidy music server over a WebSocket", "homepage": "http://www.mopidy.com/", "author": { @@ -14,19 +14,19 @@ }, "main": "src/mopidy.js", "dependencies": { - "bane": "~0.4.0", - "faye-websocket": "~0.4.4", - "when": "~2.0.0" + "bane": "~1.0.0", + "faye-websocket": "~0.7.0", + "when": "~2.4.0" }, "devDependencies": { - "buster": "~0.6.12", + "buster": "~0.6.13", "grunt": "~0.4.1", "grunt-buster": "~0.2.1", "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-jshint": "~0.4.3", - "grunt-contrib-uglify": "~0.2.0", - "grunt-contrib-watch": "~0.4.3", - "phantomjs": "~1.9.0" + "grunt-contrib-jshint": "~0.6.4", + "grunt-contrib-uglify": "~0.2.4", + "grunt-contrib-watch": "~0.5.3", + "phantomjs": "~1.9.2-0" }, "scripts": { "test": "grunt test", diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 0118395c..aa0c751e 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -17,12 +17,6 @@ mopidy_args = sys.argv[1:] sys.argv[1:] = [] -# Add ../ to the path so we can run Mopidy from a Git checkout without -# installing it on the system. -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) - - from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib @@ -42,15 +36,12 @@ def main(): if args.show_deps: commands.show_deps() - loop = gobject.MainLoop() - enabled_extensions = [] # Make sure it is defined before the finally block - logging_initialized = False - # TODO: figure out a way to make the boilerplate in this file reusable in # scanner and other places we need it. try: # Initial config without extensions to bootstrap logging. + logging_initialized = False logging_config, _ = config_lib.load( args.config_files, [], args.config_overrides) @@ -59,12 +50,16 @@ def main(): logging_config, args.verbosity_level, args.save_debug_log) logging_initialized = True + create_file_structures() + check_old_locations() + installed_extensions = ext.load_extensions() config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) # Filter out disabled extensions and remove any config errors for them. + enabled_extensions = [] for extension in installed_extensions: enabled = config[extension.ext_name]['enabled'] if ext.validate_extension(extension) and enabled: @@ -79,31 +74,38 @@ def main(): proxied_config = config_lib.Proxy(config) log.setup_log_levels(proxied_config) - create_file_structures() - check_old_locations() ext.register_gstreamer_elements(enabled_extensions) # Anything that wants to exit after this point must use # mopidy.utils.process.exit_process as actors have been started. - audio = setup_audio(proxied_config) - backends = setup_backends(proxied_config, enabled_extensions, audio) - core = setup_core(audio, backends) - setup_frontends(proxied_config, enabled_extensions, core) - loop.run() + start(proxied_config, enabled_extensions) except KeyboardInterrupt: - if logging_initialized: - logger.info('Interrupted. Exiting...') + pass except Exception as ex: if logging_initialized: logger.exception(ex) raise - finally: - loop.quit() - stop_frontends(enabled_extensions) - stop_core() - stop_backends(enabled_extensions) - stop_audio() - process.stop_remaining_actors() + + +def create_file_structures(): + path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') + path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') + + +def check_old_locations(): + dot_mopidy_dir = path.expand_path(b'~/.mopidy') + if os.path.isdir(dot_mopidy_dir): + logger.warning( + 'Old Mopidy dot dir found at %s. Please migrate your config to ' + 'the ini-file based config format. See release notes for further ' + 'instructions.', dot_mopidy_dir) + + old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py') + if os.path.isfile(old_settings_file): + logger.warning( + 'Old Mopidy settings file found at %s. Please migrate your ' + 'config to the ini-file based config format. See release notes ' + 'for further instructions.', old_settings_file) def log_extension_info(all_extensions, enabled_extensions): @@ -125,28 +127,27 @@ def check_config_errors(errors): sys.exit(1) -def check_old_locations(): - dot_mopidy_dir = path.expand_path(b'~/.mopidy') - if os.path.isdir(dot_mopidy_dir): - logger.warning( - 'Old Mopidy dot dir found at %s. Please migrate your config to ' - 'the ini-file based config format. See release notes for further ' - 'instructions.', dot_mopidy_dir) - - old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py') - if os.path.isfile(old_settings_file): - logger.warning( - 'Old Mopidy settings file found at %s. Please migrate your ' - 'config to the ini-file based config format. See release notes ' - 'for further instructions.', old_settings_file) +def start(config, extensions): + loop = gobject.MainLoop() + try: + audio = start_audio(config) + backends = start_backends(config, extensions, audio) + core = start_core(audio, backends) + start_frontends(config, extensions, core) + loop.run() + except KeyboardInterrupt: + logger.info('Interrupted. Exiting...') + return + finally: + loop.quit() + stop_frontends(extensions) + stop_core() + stop_backends(extensions) + stop_audio() + process.stop_remaining_actors() -def create_file_structures(): - path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') - path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') - - -def setup_audio(config): +def start_audio(config): logger.info('Starting Mopidy audio') return Audio.start(config=config).proxy() @@ -156,7 +157,7 @@ def stop_audio(): process.stop_actors_by_class(Audio) -def setup_backends(config, extensions, audio): +def start_backends(config, extensions, audio): backend_classes = [] for extension in extensions: backend_classes.extend(extension.get_backend_classes()) @@ -180,7 +181,7 @@ def stop_backends(extensions): process.stop_actors_by_class(backend_class) -def setup_core(audio, backends): +def start_core(audio, backends): logger.info('Starting Mopidy core') return Core.start(audio=audio, backends=backends).proxy() @@ -190,7 +191,7 @@ def stop_core(): process.stop_actors_by_class(Core) -def setup_frontends(config, extensions, core): +def start_frontends(config, extensions, core): frontend_classes = [] for extension in extensions: frontend_classes.extend(extension.get_frontend_classes()) diff --git a/mopidy/config/types.py b/mopidy/config/types.py index d3cd2462..d264de30 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -82,30 +82,22 @@ class String(ConfigValue): return encode(value) -class Secret(ConfigValue): - """Secret value. +class Secret(String): + """Secret string value. - Should be used for passwords, auth tokens etc. Deserializing will not - convert to unicode. Will mask value when being displayed. + Is decoded as utf-8 and \\n \\t escapes should work and be preserved. + + Should be used for passwords, auth tokens etc. Will mask value when being + displayed. """ def __init__(self, optional=False, choices=None): self._required = not optional - - def deserialize(self, value): - value = value.strip() - validators.validate_required(value, self._required) - if not value: - return None - return value + self._choices = None # Choices doesn't make sense for secrets def serialize(self, value, display=False): - if isinstance(value, unicode): - value = value.encode('utf-8') - if value is None: - return b'' - elif display: + if value is not None and display: return b'********' - return value + return super(Secret, self).serialize(value, display) class Integer(ConfigValue): diff --git a/mopidy/frontends/http/data/mopidy.js b/mopidy/frontends/http/data/mopidy.js index 1669eaff..3e4e832e 100644 --- a/mopidy/frontends/http/data/mopidy.js +++ b/mopidy/frontends/http/data/mopidy.js @@ -1,8 +1,8 @@ -/*! Mopidy.js - built 2013-03-31 +/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -((typeof define === "function" && define.amd && function (m) { define(m); }) || +((typeof define === "function" && define.amd && function (m) { define("bane", m); }) || (typeof module === "object" && function (m) { module.exports = m(); }) || function (m) { this.bane = m(); } )(function () { @@ -148,7 +148,7 @@ notifyListener(event, toNotify[i], args); } - toNotify = listeners(this, event).slice() + toNotify = listeners(this, event).slice(); args = slice.call(arguments, 1); for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); @@ -187,27 +187,30 @@ if (typeof window !== "undefined") { * * @author Brian Cavalier * @author John Hann - * @version 2.0.0 + * @version 2.4.0 */ -(function(define) { 'use strict'; -define(function () { +(function(define, global) { 'use strict'; +define(function (require) { // Public API - when.defer = defer; // Create a deferred + when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair when.join = join; // Join 2 or more promises when.all = all; // Resolve a list of promises when.map = map; // Array.map() for promises when.reduce = reduce; // Array.reduce() for promises + when.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. @@ -235,13 +238,35 @@ define(function () { * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state * @name Promise */ - function Promise(then) { - this.then = then; + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; } Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -262,9 +287,7 @@ define(function () { * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { - var self = this; - - return this.then(injectHandler, injectHandler).yield(self); + return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); @@ -285,6 +308,16 @@ define(function () { }); }, + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list @@ -340,13 +373,16 @@ define(function () { } /** - * Creates a new Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise - * only has then. + * has then plus extended promise API. * * @return {{ * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, @@ -394,12 +430,26 @@ define(function () { /** * Creates a new promise whose fate is determined by resolver. - * @private (for now) * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { - var value, handlers = []; + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; // Call the provider resolver to seal the promise's fate try { @@ -409,29 +459,34 @@ define(function () { } // Return the promise - return new Promise(then); + return self; /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify */ - function then(onFulfilled, onRejected, onProgress) { - return promise(function(resolve, reject, notify) { - handlers - // Call handlers later, after resolution - ? handlers.push(function(value) { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }) - // Call handlers soon, but not in the current stack - : enqueue(function() { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }); - }); + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); } /** @@ -440,14 +495,17 @@ define(function () { * @param {*|Promise} val resolution value */ function promiseResolve(val) { - if(!handlers) { + if(!consumers) { return; } value = coerce(val); - scheduleHandlers(handlers, value); + scheduleConsumers(consumers, value); + consumers = undef; - handlers = undef; + if(status) { + updateStatus(value, status); + } } /** @@ -463,27 +521,90 @@ define(function () { * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { - if(handlers) { - scheduleHandlers(handlers, progressing(update)); + if(consumers) { + scheduleConsumers(consumers, progressed(update)); } } } + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce - * @returns {Promise} Guaranteed to return a trusted Promise. If x + * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { - if(x instanceof Promise) { + if (x instanceof Promise) { return x; - } else if (x !== Object(x)) { + } + + if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } @@ -510,61 +631,34 @@ define(function () { } /** - * Create an already-fulfilled promise for the supplied value - * @private + * Proxy for a near, fulfilled value * @param {*} value - * @return {Promise} fulfilled promise + * @constructor */ - function fulfilled(value) { - var self = new Promise(function (onFulfilled) { - try { - return typeof onFulfilled == 'function' - ? coerce(onFulfilled(value)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearFulfilledProxy(value) { + this.value = value; } + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + /** - * Create an already-rejected promise with the supplied rejection reason. - * @private + * Proxy for a near rejection * @param {*} reason - * @return {Promise} rejected promise + * @constructor */ - function rejected(reason) { - var self = new Promise(function (_, onRejected) { - try { - return typeof onRejected == 'function' - ? coerce(onRejected(reason)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearRejectedProxy(reason) { + this.reason = reason; } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressing(update) { - var self = new Promise(function (_, __, onProgress) { - try { - return typeof onProgress == 'function' - ? progressing(onProgress(update)) : self; - } catch (e) { - return progressing(e); - } - }); - - return self; - } + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; /** * Schedule a task that will process a list of handlers @@ -573,7 +667,7 @@ define(function () { * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ - function scheduleHandlers(handlers, value) { + function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { @@ -582,14 +676,23 @@ define(function () { }); } + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + /** - * Determines if promiseOrValue is a promise or not - * - * @param {*} promiseOrValue anything - * @returns {boolean} true if promiseOrValue is a {@link Promise} + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like */ - function isPromise(promiseOrValue) { - return promiseOrValue && typeof promiseOrValue.then === 'function'; + function isPromiseLike(x) { + return x && typeof x.then === 'function'; } /** @@ -601,17 +704,15 @@ define(function () { * @param {Array} promisesOrValues array of anything, may contain a mix * of promises and values * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to an array of howMany values that * resolved first, or will reject with an array of * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - checkCallbacks(2, arguments); - return when(promisesOrValues, function(promisesOrValues) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); @@ -635,7 +736,7 @@ define(function () { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; reject(reasons); } }; @@ -644,7 +745,7 @@ define(function () { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; resolve(values); } }; @@ -674,9 +775,9 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ @@ -697,14 +798,13 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - checkCallbacks(1, arguments); - return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** @@ -713,28 +813,49 @@ define(function () { * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { - return map(arguments, identity); + return _map(arguments, identity); } /** - * Traditional map function, similar to `Array.prototype.map()`, but allows - * input to contain {@link Promise}s and/or values, and mapFunc may return - * either a value or a {@link Promise} - * - * @param {Array|Promise} array array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function} mapFunc mapping function mapFunc(value) which may return - * either a {@link Promise} or value - * @returns {Promise} a {@link Promise} that will resolve to an array containing - * the mapped output values. + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. */ function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { return when(array, function(array) { - return promise(resolveMap); + return _promise(resolveMap); function resolveMap(resolve, reject, notify) { - var results, len, toResolve, resolveOne, i; + var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. @@ -743,27 +864,28 @@ define(function () { if(!toResolve) { resolve(results); - } else { + return; + } - resolveOne = function(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + notify(mapped); + + if(!--toResolve) { + resolve(results); + } + }, reject); + } } }); } @@ -803,12 +925,46 @@ define(function () { }); } + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + // - // Utilities, etc. + // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, - timeout, funcProto, call, arrayProto, undef; + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, undef; + + cjsRequire = require; // // Shared handler queue processing @@ -826,20 +982,13 @@ define(function () { */ function enqueue(task) { if(handlerQueue.push(task) === 1) { - scheduleDrainQueue(); + nextTick(drainQueue); } } /** - * Schedule the queue to be drained in the next tick. - */ - function scheduleDrainQueue() { - nextTick(drainQueue); - } - - /** - * Drain the handler queue entirely or partially, being careful to allow - * the queue to be extended while it is being processed, and to continue + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { @@ -852,20 +1001,36 @@ define(function () { handlerQueue = []; } - // - // Capture function and array utils - // - /*global setImmediate:true*/ + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; - // capture setTimeout to avoid being caught by fake timers used in time based tests - timeout = setTimeout; - nextTick = typeof setImmediate === 'function' - ? typeof window === 'undefined' - ? setImmediate - : setImmediate.bind(window) - : typeof process === 'object' - ? process.nextTick - : function(task) { timeout(task, 0); }; + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Prefer setImmediate or MessageChannel, cascade to node, + // vertx and finally setTimeout + /*global setImmediate,MessageChannel,process*/ + if (typeof setImmediate === 'function') { + nextTick = setImmediate.bind(global); + } else if(typeof MessageChannel !== 'undefined') { + var channel = new MessageChannel(); + channel.port1.onmessage = drainQueue; + nextTick = function() { channel.port2.postMessage(0); }; + } else if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // // Safe function calls funcProto = Function.prototype; @@ -926,43 +1091,13 @@ define(function () { return reduced; }; - // - // Utility functions - // - - /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. - */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } - } - - function noop() {} - function identity(x) { return x; } return when; }); -})( - typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } -); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); if (typeof module === "object" && typeof require === "function") { var bane = require("bane"); diff --git a/mopidy/frontends/http/data/mopidy.min.js b/mopidy/frontends/http/data/mopidy.min.js index 08ee1dac..75d9fff1 100644 --- a/mopidy/frontends/http/data/mopidy.min.js +++ b/mopidy/frontends/http/data/mopidy.min.js @@ -1,5 +1,5 @@ -/*! Mopidy.js - built 2013-03-31 +/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -function Mopidy(e){return this instanceof Mopidy?(this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(e)}if(("function"==typeof define&&define.amd&&function(e){define(e)}||"object"==typeof module&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function e(e,t,n){var o,i=n.length;if(i>0)for(o=0;i>o;++o)n[o](e,t);else setTimeout(function(){throw t.message=e+" listener threw error: "+t.message,t},0)}function t(e){if("function"!=typeof e)throw new TypeError("Listener is not function");return e}function n(e){return e.supervisors||(e.supervisors=[]),e.supervisors}function o(e,t){return e.listeners||(e.listeners={}),t&&!e.listeners[t]&&(e.listeners[t]=[]),t?e.listeners[t]:e.listeners}function i(e){return e.errbacks||(e.errbacks=[]),e.errbacks}function r(r){function c(t,n,o){try{n.listener.apply(n.thisp||r,o)}catch(s){e(t,s,i(r))}}return r=r||{},r.on=function(e,i,r){return"function"==typeof e?n(this).push({listener:e,thisp:i}):(o(this,e).push({listener:t(i),thisp:r}),void 0)},r.off=function(e,t){var r,s,c,f;if(!e){r=n(this),r.splice(0,r.length),s=o(this);for(c in s)s.hasOwnProperty(c)&&(r=o(this,c),r.splice(0,r.length));return r=i(this),r.splice(0,r.length),void 0}if("function"==typeof e?(r=n(this),t=e):r=o(this,e),!t)return r.splice(0,r.length),void 0;for(c=0,f=r.length;f>c;++c)if(r[c].listener===t)return r.splice(c,1),void 0},r.once=function(e,t,n){var o=function(){r.off(e,o),t.apply(this,arguments)};r.on(e,o,n)},r.bind=function(e,t){var n,o,i;if(t)for(o=0,i=t.length;i>o;++o){if("function"!=typeof e[t[o]])throw Error("No such method "+t[o]);this.on(t[o],e[t[o]],e)}else for(n in e)"function"==typeof e[n]&&this.on(n,e[n],e);return e},r.emit=function(e){var t,i,r=n(this),f=s.call(arguments);for(t=0,i=r.length;i>t;++t)c(e,r[t],f);for(r=o(this,e).slice(),f=s.call(arguments,1),t=0,i=r.length;i>t;++t)c(e,r[t],f)},r.errback=function(e){this.errbacks||(this.errbacks=[]),this.errbacks.push(t(e))},r}var s=Array.prototype.slice;return{createEventEmitter:r}}),"undefined"!=typeof window&&(window.define=function(e){try{delete window.define}catch(t){window.define=void 0}window.when=e()},window.define.amd={}),function(e){"use strict";e(function(){function e(e,t,o,i){return n(e).then(t,o,i)}function t(e){this.then=e}function n(e){return r(function(t){t(e)})}function o(t){return e(t,f)}function i(){function e(e,r,s){t.resolve=t.resolver.resolve=function(t){return i?n(t):(i=!0,e(t),o)},t.reject=t.resolver.reject=function(e){return i?n(f(e)):(i=!0,r(e),o)},t.notify=t.resolver.notify=function(e){return s(e),e}}var t,o,i;return t={promise:R,resolve:R,reject:R,notify:R,resolver:{resolve:R,reject:R,notify:R}},t.promise=o=r(e),t}function r(e){function n(e,t,n){return r(function(o,i,r){p?p.push(function(s){s.then(e,t,n).then(o,i,r)}):k(function(){h.then(e,t,n).then(o,i,r)})})}function o(e){p&&(h=s(e),a(p,h),p=R)}function i(e){o(f(e))}function c(e){p&&a(p,u(e))}var h,p=[];try{e(o,i,c)}catch(l){i(l)}return new t(n)}function s(e){return e instanceof t?e:e!==Object(e)?c(e):r(function(t,n,o){k(function(){try{var i=e.then;"function"==typeof i?j(i,e,t,n,o):t(c(e))}catch(r){n(r)}})})}function c(e){var n=new t(function(t){try{return"function"==typeof t?s(t(e)):n}catch(o){return f(o)}});return n}function f(e){var n=new t(function(t,o){try{return"function"==typeof o?s(o(e)):n}catch(i){return f(i)}});return n}function u(e){var n=new t(function(t,o,i){try{return"function"==typeof i?u(i(e)):n}catch(r){return u(r)}});return n}function a(e,t){k(function(){for(var n,o=0;n=e[o++];)n(t)})}function h(e){return e&&"function"==typeof e.then}function p(t,n,o,i,s){return m(2,arguments),e(t,function(t){function c(o,i,r){function s(e){l(e)}function c(e){p(e)}var f,u,a,h,p,l,d,y;if(d=t.length>>>0,f=Math.max(0,Math.min(n,d)),a=[],u=d-f+1,h=[],f)for(l=function(e){h.push(e),--u||(p=l=v,i(h))},p=function(e){a.push(e),--f||(p=l=v,o(a))},y=0;d>y;++y)y in t&&e(t[y],c,s,r);else o(a)}return r(c).then(o,i,s)})}function l(e,t,n,o){function i(e){return t?t(e[0]):e[0]}return p(e,1,i,n,o)}function d(e,t,n,o){return m(1,arguments),b(e,M).then(t,n,o)}function y(){return b(arguments,M)}function b(t,n){return e(t,function(t){function o(o,i,r){var s,c,f,u,a;if(f=c=t.length>>>0,s=[],f)for(u=function(t,c){e(t,n).then(function(e){s[c]=e,--f||o(s)},i,r)},a=0;c>a;a++)a in t?u(t[a],a):--f;else o(s)}return r(o)})}function w(t,n){var o=j(E,arguments,1);return e(t,function(t){var i;return i=t.length,o[0]=function(t,o,r){return e(t,function(t){return e(o,function(e){return n(t,e,r,i)})})},S.apply(t,o)})}function k(e){1===W.push(e)&&g()}function g(){D(_)}function _(){for(var e,t=0;e=W[t++];)e();W=[]}function m(e,t){for(var n,o=t.length;o>e;)if(n=t[--o],null!=n&&"function"!=typeof n)throw Error("arg "+o+" must be a function")}function v(){}function M(e){return e}e.defer=i,e.resolve=n,e.reject=o,e.join=y,e.all=d,e.map=b,e.reduce=w,e.any=l,e.some=p,e.isPromise=h,t.prototype={otherwise:function(e){return this.then(R,e)},ensure:function(e){function t(){return n(e())}var o=this;return this.then(t,t).yield(o)},yield:function(e){return this.then(function(){return e})},spread:function(e){return this.then(function(t){return d(t,function(t){return e.apply(R,t)})})},always:function(e,t){return this.then(e,e,t)}};var S,E,j,D,W,O,q,C,x,R;return W=[],O=setTimeout,D="function"==typeof setImmediate?"undefined"==typeof window?setImmediate:setImmediate.bind(window):"object"==typeof process?process.nextTick:function(e){O(e,0)},q=Function.prototype,C=q.call,j=q.bind?C.bind(C):function(e,t){return e.apply(t,E.call(arguments,2))},x=[],E=x.slice,S=x.reduce||function(e){var t,n,o,i,r;if(r=0,t=Object(this),i=t.length>>>0,n=arguments,1>=n.length)for(;;){if(r in t){o=t[r++];break}if(++r>=i)throw new TypeError}else o=n[1];for(;i>r;++r)r in t&&(o=e(o,t[r],r,t));return o},e})}("function"==typeof define&&define.amd?define:function(e){module.exports=e()}),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(e){var t="undefined"!=typeof document&&document.location.host||"localhost";return e.webSocketUrl=e.webSocketUrl||"ws://"+t+"/mopidy/ws/",e.autoConnect!==!1&&(e.autoConnect=!0),e.backoffDelayMin=e.backoffDelayMin||1e3,e.backoffDelayMax=e.backoffDelayMax||64e3,e},Mopidy.prototype._getConsole=function(){var e=e!==void 0&&e||{};return e.log=e.log||function(){},e.warn=e.warn||function(){},e.error=e.error||function(){},e},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(e){this.emit("websocket:close",e)}.bind(this),this._webSocket.onerror=function(e){this.emit("websocket:error",e)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(e){this.emit("websocket:incomingMessage",e)}.bind(this)},Mopidy.prototype._cleanup=function(e){Object.keys(this._pendingRequests).forEach(function(t){var n=this._pendingRequests[t];delete this._pendingRequests[t],n.reject({message:"WebSocket closed",closeEvent:e})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id))return this._console.warn("Unexpected response received. Message was:",e),void 0;var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&"core"===t[0]&&(t=t.slice(1)),t},o=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),i=function(i){var r=n(i),s=this._snakeToCamel(r.slice(-1)[0]),c=o(r.slice(0,-1));c[s]=t(i),c[s].description=e[i].description,c[s].params=e[i].params}.bind(this);Object.keys(e).forEach(i),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file +function Mopidy(a){return this instanceof Mopidy?(this._settings=this._configure(a||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(a)}if(("function"==typeof define&&define.amd&&function(a){define("bane",a)}||"object"==typeof module&&function(a){module.exports=a()}||function(a){this.bane=a()})(function(){"use strict";function a(a,b,c){var d,e=c.length;if(e>0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):(d(this,a).push({listener:b(e),thisp:f}),void 0)},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),f.splice(0,f.length),void 0}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return f.splice(0,f.length),void 0;for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return f.splice(h,1),void 0},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f}}),"undefined"!=typeof window&&(window.define=function(a){try{delete window.define}catch(b){window.define=void 0}window.when=a()},window.define.amd={}),function(a,b){"use strict";a(function(a){function c(a,b,c,d){return e(a).then(b,c,d)}function d(a,b){this._message=a,this.inspect=b}function e(a){return h(function(b){b(a)})}function f(a){return c(a,k)}function g(){function a(a,f,g){b.resolve=b.resolver.resolve=function(b){return d?e(b):(d=!0,a(b),c)},b.reject=b.resolver.reject=function(a){return d?e(k(a)):(d=!0,f(a),c)},b.notify=b.resolver.notify=function(a){return g(a),a}}var b,c,d;return b={promise:S,resolve:S,reject:S,notify:S,resolver:{resolve:S,reject:S,notify:S}},b.promise=c=h(a),b}function h(a){return i(a,Q.PromiseStatus&&Q.PromiseStatus())}function i(a,b){function c(a,b,c,d){function e(e){e._message(a,b,c,d)}l?l.push(e):E(function(){e(j)})}function e(){return j?j.inspect():D()}function f(a){l&&(j=n(a),q(l,j),l=S,b&&r(j,b))}function g(a){f(k(a))}function h(a){l&&q(l,m(a))}var i,j,l=[];i=new d(c,e),i._status=b;try{a(f,g,h)}catch(o){g(o)}return i}function j(a){return l(new o(a),function(){return B(a)})}function k(a){return l(new p(a),function(){return C(a)})}function l(a,b){return new d(function(b,c,d){try{d(a[b].apply(a,c))}catch(e){d(k(e))}},b)}function m(a){return new d(function(b,c,d,e){var f=c[2];try{e("function"==typeof f?f(a):a)}catch(g){e(g)}})}function n(a){return a instanceof d?a:a===Object(a)&&"then"in a?h(function(b,c,d){E(function(){try{var e=a.then;"function"==typeof e?J(e,a,b,c,d):b(j(a))}catch(f){c(f)}})}):j(a)}function o(a){this.value=a}function p(a){this.reason=a}function q(a,b){E(function(){for(var c,d=0;c=a[d++];)c(b)})}function r(a,b){function c(){b.fulfilled()}function d(a){b.rejected(a)}a.then(c,d)}function s(a){return a&&"function"==typeof a.then}function t(a,b,d,e,f){return c(a,function(a){function g(d,e,f){function g(a){n(a)}function h(a){m(a)}var i,j,k,l,m,n,o,p;if(o=a.length>>>0,i=Math.max(0,Math.min(b,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=G,e(l))},m=function(a){k.push(a),--i||(m=n=G,d(k))},p=0;o>p;++p)p in a&&c(a[p],h,g,f);else d(k)}return h(g).then(d,e,f)})}function u(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return t(a,1,e,c,d)}function v(a,b,c,d){return z(a,G).then(b,c,d)}function w(){return z(arguments,G)}function x(a){return z(a,B,C)}function y(a,b){return z(a,b)}function z(a,b,d){return c(a,function(a){function e(e,f,g){function h(a,h){c(a,b,d).then(function(a){i[h]=a,g(a),--k||e(i)},f)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return i(e)})}function A(a,b){var d=J(I,arguments,1);return c(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return c(a,function(a){return c(d,function(c){return b(a,c,f,e)})})},H.apply(a,d)})}function B(a){return{state:"fulfilled",value:a}}function C(a){return{state:"rejected",reason:a}}function D(){return{state:"pending"}}function E(a){1===L.push(a)&&K(F)}function F(){for(var a,b=0;a=L[b++];)a();L=[]}function G(a){return a}c.promise=h,c.resolve=e,c.reject=f,c.defer=g,c.join=w,c.all=v,c.map=y,c.reduce=A,c.settle=x,c.any=u,c.some=t,c.isPromise=s,c.isPromiseLike=s,d.prototype={then:function(){var a,b;return a=arguments,b=this._message,i(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(S,a)},ensure:function(a){function b(){return e(a())}return this.then(b,b).yield(this)},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return v(b,function(b){return a.apply(S,b)})})},always:function(a,b){return this.then(a,a,b)}},o.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},p.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var H,I,J,K,L,M,N,O,P,Q,R,S;if(R=a,L=[],M=b.setTimeout,Q="undefined"!=typeof console?console:c,"function"==typeof setImmediate)K=setImmediate.bind(b);else if("undefined"!=typeof MessageChannel){var T=new MessageChannel;T.port1.onmessage=F,K=function(){T.port2.postMessage(0)}}else if("object"==typeof process&&process.nextTick)K=process.nextTick;else try{K=R("vertx").runOnLoop||R("vertx").runOnContext}catch(U){K=function(a){M(a,0)}}return N=Function.prototype,O=N.call,J=N.bind?O.bind(O):function(a,b){return a.apply(b,I.call(arguments,2))},P=[],I=P.slice,H=P.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},c})}("function"==typeof define&&define.amd?define:function(a){module.exports=a(require)},this),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(a){var b="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||"ws://"+b+"/mopidy/ws/",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,a},Mopidy.prototype._getConsole=function(){var a="undefined"!=typeof a&&a||{};return a.log=a.log||function(){},a.warn=a.warn||function(){},a.error=a.error||function(){},a},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},Mopidy.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var c=this._pendingRequests[b];delete this._pendingRequests[b],c.reject({message:"WebSocket closed",closeEvent:a})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},Mopidy.prototype._send=function(a){var b=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:b.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:b.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:b.resolver.reject({message:"WebSocket is closed"});break;default:a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a)}return b.promise},Mopidy.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),Mopidy.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},Mopidy.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return this._console.warn("Unexpected response received. Message was:",a),void 0;var b=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?b.resolve(a.result):a.hasOwnProperty("error")?(b.reject(a.error),this._console.warn("Server returned error:",a.error)):(b.reject({message:"Response without 'result' or 'error' received",data:{response:a}}),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},Mopidy.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(a){var b=function(a){return function(){var b=Array.prototype.slice.call(arguments);return this._send({method:a,params:b})}.bind(this)}.bind(this),c=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},d=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),e=function(e){var f=c(e),g=this._snakeToCamel(f.slice(-1)[0]),h=d(f.slice(0,-1));h[g]=b(e),h[g].description=a[e].description,h[g].params=a[e].params}.bind(this);Object.keys(a).forEach(e),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file diff --git a/mopidy/models.py b/mopidy/models.py index fe390ddf..3fc92bb4 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -90,7 +90,7 @@ class ImmutableObject(object): for v in value] elif isinstance(value, ImmutableObject): value = value.serialize() - if value: + if not (isinstance(value, list) and len(value) == 0): data[public_key] = value return data diff --git a/tests/config/types_test.py b/tests/config/types_test.py index 74e9672d..0df3dfb4 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -105,11 +105,11 @@ class StringTest(unittest.TestCase): class SecretTest(unittest.TestCase): - def test_deserialize_passes_through(self): + def test_deserialize_decodes_utf8(self): value = types.Secret() - result = value.deserialize(b'foo') - self.assertIsInstance(result, bytes) - self.assertEqual(b'foo', result) + result = value.deserialize('æøå'.encode('utf-8')) + self.assertIsInstance(result, unicode) + self.assertEqual('æøå', result) def test_deserialize_enforces_required(self): value = types.Secret() diff --git a/tests/help_test.py b/tests/help_test.py index 4f210031..574e4fd7 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -12,7 +12,10 @@ class HelpTest(unittest.TestCase): def test_help_has_mopidy_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) args = [sys.executable, mopidy_dir, '--help'] - process = subprocess.Popen(args, stdout=subprocess.PIPE) + process = subprocess.Popen( + args, + env={'PYTHONPATH': os.path.join(mopidy_dir, '..')}, + stdout=subprocess.PIPE) output = process.communicate()[0] self.assertIn('--version', output) self.assertIn('--help', output) diff --git a/tests/models_test.py b/tests/models_test.py index a0fe08c7..afd1858b 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -95,6 +95,11 @@ class ArtistTest(unittest.TestCase): {'__model__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) + def test_serialize_falsy_values(self): + self.assertDictEqual( + {'__model__': 'Artist', 'uri': '', 'name': None}, + Artist(uri='', name=None).serialize()) + def test_to_json_and_back(self): artist1 = Artist(uri='uri', name='name') serialized = json.dumps(artist1, cls=ModelJSONEncoder)