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)