Merge pull request #262 from jodal/feature/mopidy.js
Mopidy.js: Mopidy's core API in JavaScript
This commit is contained in:
commit
4cf1e5ffb8
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,4 +11,5 @@ coverage.xml
|
||||
dist/
|
||||
docs/_build/
|
||||
mopidy.log*
|
||||
node_modules/
|
||||
nosetests.xml
|
||||
|
||||
75
js/README.rst
Normal file
75
js/README.rst
Normal file
@ -0,0 +1,75 @@
|
||||
*********
|
||||
Mopidy.js
|
||||
*********
|
||||
|
||||
This is the source for the JavaScript library that is installed as a part of
|
||||
Mopidy's HTTP frontend. The library makes Mopidy's core API available from the
|
||||
browser, using JSON-RPC messages over a WebSocket to communicate with Mopidy.
|
||||
|
||||
|
||||
Getting it
|
||||
==========
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
In the source repo, you can find the files at:
|
||||
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Building from source
|
||||
====================
|
||||
|
||||
1. Install `Node.js <http://nodejs.org/>`_ and npm. There is a PPA if you're
|
||||
running Ubuntu::
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
2. Assuming you install from PPA, setup your ``NODE_PATH`` environment variable
|
||||
to include ``/usr/lib/node_modules``. Add the following to your
|
||||
``~/.bashrc`` or equivalent::
|
||||
|
||||
export NODE_PATH=/usr/lib/node_modules:$NODE_PATH
|
||||
|
||||
3. Install `Buster.js <http://busterjs.org/>`_ and `Grunt
|
||||
<http://gruntjs.com/>`_ globally (or locally, and make sure you get their
|
||||
binaries on your ``PATH``)::
|
||||
|
||||
sudo npm -g install buster grunt
|
||||
|
||||
4. Install the grunt-buster Grunt plugin locally, when in the ``js/`` dir::
|
||||
|
||||
cd js/
|
||||
npm install grunt-buster
|
||||
|
||||
5. Install `PhantomJS <http://phantomjs.org/>`_ so that we can run the tests
|
||||
without a browser::
|
||||
|
||||
sudo apt-get install phantomjs
|
||||
|
||||
It is packaged in Ubuntu since 12.04, but I haven't tested with versions
|
||||
older than 1.6 which is the one packaged in Ubuntu 12.10.
|
||||
|
||||
6. Run Grunt to lint, test, concatenate, and minify the source::
|
||||
|
||||
grunt
|
||||
|
||||
The files in ``../mopidy/frontends/http/data/`` should now be up to date.
|
||||
|
||||
|
||||
Development tips
|
||||
================
|
||||
|
||||
If you're coding on the JavaScript library, you should know about ``grunt
|
||||
watch``. It lints and tests the code every time you save a file.
|
||||
9
js/buster.js
Normal file
9
js/buster.js
Normal file
@ -0,0 +1,9 @@
|
||||
var config = module.exports;
|
||||
|
||||
config["tests"] = {
|
||||
environment: "browser",
|
||||
libs: ["lib/**/*.js"],
|
||||
sources: ["src/**/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
};
|
||||
65
js/grunt.js
Normal file
65
js/grunt.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*global module:false*/
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
meta: {
|
||||
banner: "/*! Mopidy.js - built " +
|
||||
"<%= grunt.template.today('yyyy-mm-dd') %>\n" +
|
||||
" * http://www.mopidy.com/\n" +
|
||||
" * Copyright (c) <%= grunt.template.today('yyyy') %> " +
|
||||
"Stein Magnus Jodal and contributors\n" +
|
||||
" * Licensed under the Apache License, Version 2.0 */"
|
||||
},
|
||||
dirs: {
|
||||
dest: "../mopidy/frontends/http/data"
|
||||
},
|
||||
lint: {
|
||||
files: ["grunt.js", "src/**/*.js", "test/**/*-test.js"]
|
||||
},
|
||||
buster: {
|
||||
test: {
|
||||
config: "buster.js"
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
dist: {
|
||||
src: ["<banner:meta.banner>", "lib/**/*.js", "src/mopidy.js"],
|
||||
dest: "<%= dirs.dest %>/mopidy.js"
|
||||
}
|
||||
},
|
||||
min: {
|
||||
dist: {
|
||||
src: ["<banner:meta.banner>", "<config:concat.dist.dest>"],
|
||||
dest: "<%= dirs.dest %>/mopidy.min.js"
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files: "<config:lint.files>",
|
||||
tasks: "lint buster concat min"
|
||||
},
|
||||
jshint: {
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
indent: 4,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
quotmark: "double",
|
||||
undef: true,
|
||||
unused: true,
|
||||
eqnull: true,
|
||||
browser: true,
|
||||
devel: true
|
||||
},
|
||||
globals: {}
|
||||
},
|
||||
uglify: {}
|
||||
});
|
||||
|
||||
grunt.registerTask("default", "lint buster concat min");
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
};
|
||||
171
js/lib/bane-0.4.0.js
Normal file
171
js/lib/bane-0.4.0.js
Normal file
@ -0,0 +1,171 @@
|
||||
/**
|
||||
* BANE - Browser globals, AMD and Node Events
|
||||
*
|
||||
* https://github.com/busterjs/bane
|
||||
*
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
((typeof define === "function" && define.amd && function (m) { define(m); }) ||
|
||||
(typeof module === "object" && function (m) { module.exports = m(); }) ||
|
||||
function (m) { this.bane = m(); }
|
||||
)(function () {
|
||||
"use strict";
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
function handleError(event, error, errbacks) {
|
||||
var i, l = errbacks.length;
|
||||
if (l > 0) {
|
||||
for (i = 0; i < l; ++i) { errbacks[i](event, error); }
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
error.message = event + " listener threw error: " + error.message;
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertFunction(fn) {
|
||||
if (typeof fn !== "function") {
|
||||
throw new TypeError("Listener is not function");
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
function supervisors(object) {
|
||||
if (!object.supervisors) { object.supervisors = []; }
|
||||
return object.supervisors;
|
||||
}
|
||||
|
||||
function listeners(object, event) {
|
||||
if (!object.listeners) { object.listeners = {}; }
|
||||
if (event && !object.listeners[event]) { object.listeners[event] = []; }
|
||||
return event ? object.listeners[event] : object.listeners;
|
||||
}
|
||||
|
||||
function errbacks(object) {
|
||||
if (!object.errbacks) { object.errbacks = []; }
|
||||
return object.errbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @signature var emitter = bane.createEmitter([object]);
|
||||
*
|
||||
* Create a new event emitter. If an object is passed, it will be modified
|
||||
* by adding the event emitter methods (see below).
|
||||
*/
|
||||
function createEventEmitter(object) {
|
||||
object = object || {};
|
||||
|
||||
function notifyListener(event, listener, args) {
|
||||
try {
|
||||
listener.listener.apply(listener.thisp || object, args);
|
||||
} catch (e) {
|
||||
handleError(event, e, errbacks(object));
|
||||
}
|
||||
}
|
||||
|
||||
object.on = function (event, listener, thisp) {
|
||||
if (typeof event === "function") {
|
||||
return supervisors(this).push({
|
||||
listener: event,
|
||||
thisp: listener
|
||||
});
|
||||
}
|
||||
listeners(this, event).push({
|
||||
listener: assertFunction(listener),
|
||||
thisp: thisp
|
||||
});
|
||||
};
|
||||
|
||||
object.off = function (event, listener) {
|
||||
var fns, events, i, l;
|
||||
if (!event) {
|
||||
fns = supervisors(this);
|
||||
fns.splice(0, fns.length);
|
||||
|
||||
events = listeners(this);
|
||||
for (i in events) {
|
||||
if (events.hasOwnProperty(i)) {
|
||||
fns = listeners(this, i);
|
||||
fns.splice(0, fns.length);
|
||||
}
|
||||
}
|
||||
|
||||
fns = errbacks(this);
|
||||
fns.splice(0, fns.length);
|
||||
|
||||
return;
|
||||
}
|
||||
if (typeof event === "function") {
|
||||
fns = supervisors(this);
|
||||
listener = event;
|
||||
} else {
|
||||
fns = listeners(this, event);
|
||||
}
|
||||
if (!listener) {
|
||||
fns.splice(0, fns.length);
|
||||
return;
|
||||
}
|
||||
for (i = 0, l = fns.length; i < l; ++i) {
|
||||
if (fns[i].listener === listener) {
|
||||
fns.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
object.once = function (event, listener, thisp) {
|
||||
var wrapper = function () {
|
||||
object.off(event, wrapper);
|
||||
listener.apply(this, arguments);
|
||||
};
|
||||
|
||||
object.on(event, wrapper, thisp);
|
||||
};
|
||||
|
||||
object.bind = function (object, events) {
|
||||
var prop, i, l;
|
||||
if (!events) {
|
||||
for (prop in object) {
|
||||
if (typeof object[prop] === "function") {
|
||||
this.on(prop, object[prop], object);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i = 0, l = events.length; i < l; ++i) {
|
||||
if (typeof object[events[i]] === "function") {
|
||||
this.on(events[i], object[events[i]], object);
|
||||
} else {
|
||||
throw new Error("No such method " + events[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
object.emit = function (event) {
|
||||
var toNotify = supervisors(this);
|
||||
var args = slice.call(arguments), i, l;
|
||||
|
||||
for (i = 0, l = toNotify.length; i < l; ++i) {
|
||||
notifyListener(event, toNotify[i], args);
|
||||
}
|
||||
|
||||
toNotify = listeners(this, event).slice()
|
||||
args = slice.call(arguments, 1);
|
||||
for (i = 0, l = toNotify.length; i < l; ++i) {
|
||||
notifyListener(event, toNotify[i], args);
|
||||
}
|
||||
};
|
||||
|
||||
object.errback = function (listener) {
|
||||
if (!this.errbacks) { this.errbacks = []; }
|
||||
this.errbacks.push(assertFunction(listener));
|
||||
};
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
return { createEventEmitter: createEventEmitter };
|
||||
});
|
||||
731
js/lib/when-1.6.1.js
Normal file
731
js/lib/when-1.6.1.js
Normal file
@ -0,0 +1,731 @@
|
||||
/** @license MIT License (c) copyright B Cavalier & J Hann */
|
||||
|
||||
/**
|
||||
* A lightweight CommonJS Promises/A and when() implementation
|
||||
* when is part of the cujo.js family of libraries (http://cujojs.com/)
|
||||
*
|
||||
* Licensed under the MIT License at:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* @version 1.6.1
|
||||
*/
|
||||
|
||||
(function(define) { 'use strict';
|
||||
define(['module'], function () {
|
||||
var reduceArray, slice, undef;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
when.defer = defer; // Create a deferred
|
||||
when.resolve = resolve; // Create a resolved promise
|
||||
when.reject = reject; // Create a rejected promise
|
||||
|
||||
when.join = join; // Join 2 or more promises
|
||||
|
||||
when.all = all; // Resolve a list of promises
|
||||
when.some = some; // Resolve a sub-set of promises
|
||||
when.any = any; // Resolve one promise in a list
|
||||
|
||||
when.map = map; // Array.map() for promises
|
||||
when.reduce = reduce; // Array.reduce() for promises
|
||||
|
||||
when.chain = chain; // Make a promise trigger another resolver
|
||||
|
||||
when.isPromise = isPromise; // Determine if a thing is a promise
|
||||
|
||||
/**
|
||||
* Register an observer for a promise or immediate value.
|
||||
* @function
|
||||
* @name when
|
||||
* @namespace
|
||||
*
|
||||
* @param promiseOrValue {*}
|
||||
* @param {Function} [callback] callback to be called when promiseOrValue is
|
||||
* successfully fulfilled. If promiseOrValue is an immediate value, callback
|
||||
* will be invoked immediately.
|
||||
* @param {Function} [errback] callback to be called when promiseOrValue is
|
||||
* rejected.
|
||||
* @param {Function} [progressHandler] callback to be called when progress updates
|
||||
* are issued for promiseOrValue.
|
||||
* @returns {Promise} a new {@link Promise} that will complete with the return
|
||||
* value of callback or errback or the completion value of promiseOrValue if
|
||||
* callback and/or errback is not supplied.
|
||||
*/
|
||||
function when(promiseOrValue, callback, errback, progressHandler) {
|
||||
// Get a trusted promise for the input promiseOrValue, and then
|
||||
// register promise handlers
|
||||
return resolve(promiseOrValue).then(callback, errback, progressHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
|
||||
* promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise}
|
||||
* whose value is promiseOrValue if promiseOrValue is an immediate value.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue {*}
|
||||
* @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
|
||||
* returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
|
||||
* whose resolution value is:
|
||||
* * the resolution value of promiseOrValue if it's a foreign promise, or
|
||||
* * promiseOrValue if it's a value
|
||||
*/
|
||||
function resolve(promiseOrValue) {
|
||||
var promise, deferred;
|
||||
|
||||
if(promiseOrValue instanceof Promise) {
|
||||
// It's a when.js promise, so we trust it
|
||||
promise = promiseOrValue;
|
||||
|
||||
} else {
|
||||
// It's not a when.js promise. See if it's a foreign promise or a value.
|
||||
|
||||
// Some promises, particularly Q promises, provide a valueOf method that
|
||||
// attempts to synchronously return the fulfilled value of the promise, or
|
||||
// returns the unresolved promise itself. Attempting to break a fulfillment
|
||||
// value out of a promise appears to be necessary to break cycles between
|
||||
// Q and When attempting to coerce each-other's promises in an infinite loop.
|
||||
// For promises that do not implement "valueOf", the Object#valueOf is harmless.
|
||||
// See: https://github.com/kriskowal/q/issues/106
|
||||
// IMPORTANT: Must check for a promise here, since valueOf breaks other things
|
||||
// like Date.
|
||||
if (isPromise(promiseOrValue) && typeof promiseOrValue.valueOf === 'function') {
|
||||
promiseOrValue = promiseOrValue.valueOf();
|
||||
}
|
||||
|
||||
if(isPromise(promiseOrValue)) {
|
||||
// It looks like a thenable, but we don't know where it came from,
|
||||
// so we don't trust its implementation entirely. Introduce a trusted
|
||||
// middleman when.js promise
|
||||
deferred = defer();
|
||||
|
||||
// IMPORTANT: This is the only place when.js should ever call .then() on
|
||||
// an untrusted promise.
|
||||
promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
|
||||
promise = deferred.promise;
|
||||
|
||||
} else {
|
||||
// It's a value, not a promise. Create a resolved promise for it.
|
||||
promise = fulfilled(promiseOrValue);
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rejected promise for the supplied promiseOrValue. If
|
||||
* promiseOrValue is a value, it will be the rejection value of the
|
||||
* returned promise. If promiseOrValue is a promise, its
|
||||
* completion value will be the rejected value of the returned promise
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue {*} the rejected value of the returned {@link Promise}
|
||||
* @return {Promise} rejected {@link Promise}
|
||||
*/
|
||||
function reject(promiseOrValue) {
|
||||
return when(promiseOrValue, function(value) {
|
||||
return rejected(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted Promise constructor. A Promise created from this constructor is
|
||||
* a trusted when.js promise. Any other duck-typed promise is considered
|
||||
* untrusted.
|
||||
* @constructor
|
||||
* @name Promise
|
||||
*/
|
||||
function Promise(then) {
|
||||
this.then = then;
|
||||
}
|
||||
|
||||
Promise.prototype = {
|
||||
/**
|
||||
* Register a callback that will be called when a promise is
|
||||
* resolved or rejected. Optionally also register a progress handler.
|
||||
* Shortcut for .then(alwaysback, alwaysback, progback)
|
||||
* @memberOf Promise
|
||||
* @param alwaysback {Function}
|
||||
* @param progback {Function}
|
||||
* @return {Promise}
|
||||
*/
|
||||
always: function(alwaysback, progback) {
|
||||
return this.then(alwaysback, alwaysback, progback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a rejection handler. Shortcut for .then(null, errback)
|
||||
* @memberOf Promise
|
||||
* @param errback {Function}
|
||||
* @return {Promise}
|
||||
*/
|
||||
otherwise: function(errback) {
|
||||
return this.then(undef, errback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an already-resolved promise for the supplied value
|
||||
* @private
|
||||
*
|
||||
* @param value anything
|
||||
* @return {Promise}
|
||||
*/
|
||||
function fulfilled(value) {
|
||||
var p = new Promise(function(callback) {
|
||||
try {
|
||||
return resolve(callback ? callback(value) : value);
|
||||
} catch(e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an already-rejected {@link Promise} with the supplied
|
||||
* rejection reason.
|
||||
* @private
|
||||
*
|
||||
* @param reason rejection reason
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rejected(reason) {
|
||||
var p = new Promise(function(callback, errback) {
|
||||
try {
|
||||
return errback ? resolve(errback(reason)) : rejected(reason);
|
||||
} catch(e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new, Deferred with fully isolated resolver and promise parts,
|
||||
* either or both of which may be given out safely to consumers.
|
||||
* The Deferred itself has the full API: resolve, reject, progress, and
|
||||
* then. The resolver has resolve, reject, and progress. The promise
|
||||
* only has then.
|
||||
* @memberOf when
|
||||
* @function
|
||||
*
|
||||
* @return {Deferred}
|
||||
*/
|
||||
function defer() {
|
||||
var deferred, promise, handlers, progressHandlers,
|
||||
_then, _progress, _resolve;
|
||||
|
||||
/**
|
||||
* The promise for the new deferred
|
||||
* @type {Promise}
|
||||
*/
|
||||
promise = new Promise(then);
|
||||
|
||||
/**
|
||||
* The full Deferred object, with {@link Promise} and {@link Resolver} parts
|
||||
* @class Deferred
|
||||
* @name Deferred
|
||||
*/
|
||||
deferred = {
|
||||
then: then,
|
||||
resolve: promiseResolve,
|
||||
reject: promiseReject,
|
||||
// TODO: Consider renaming progress() to notify()
|
||||
progress: promiseProgress,
|
||||
|
||||
promise: promise,
|
||||
|
||||
resolver: {
|
||||
resolve: promiseResolve,
|
||||
reject: promiseReject,
|
||||
progress: promiseProgress
|
||||
}
|
||||
};
|
||||
|
||||
handlers = [];
|
||||
progressHandlers = [];
|
||||
|
||||
/**
|
||||
* Pre-resolution then() that adds the supplied callback, errback, and progback
|
||||
* functions to the registered listeners
|
||||
* @private
|
||||
*
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @throws {Error} if any argument is not null, undefined, or a Function
|
||||
*/
|
||||
_then = function(callback, errback, progback) {
|
||||
var deferred, progressHandler;
|
||||
|
||||
deferred = defer();
|
||||
progressHandler = progback
|
||||
? function(update) {
|
||||
try {
|
||||
// Allow progress handler to transform progress event
|
||||
deferred.progress(progback(update));
|
||||
} catch(e) {
|
||||
// Use caught value as progress
|
||||
deferred.progress(e);
|
||||
}
|
||||
}
|
||||
: deferred.progress;
|
||||
|
||||
handlers.push(function(promise) {
|
||||
promise.then(callback, errback)
|
||||
.then(deferred.resolve, deferred.reject, progressHandler);
|
||||
});
|
||||
|
||||
progressHandlers.push(progressHandler);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @private
|
||||
* @param update {*} progress event payload to pass to all listeners
|
||||
*/
|
||||
_progress = function(update) {
|
||||
processQueue(progressHandlers, update);
|
||||
return update;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the resolution or rejection
|
||||
* @private
|
||||
* @param completed {Promise} the completed value of this deferred
|
||||
*/
|
||||
_resolve = function(completed) {
|
||||
completed = resolve(completed);
|
||||
|
||||
// Replace _then with one that directly notifies with the result.
|
||||
_then = completed.then;
|
||||
// Replace _resolve so that this Deferred can only be completed once
|
||||
_resolve = resolve;
|
||||
// Make _progress a noop, to disallow progress for the resolved promise.
|
||||
_progress = noop;
|
||||
|
||||
// Notify handlers
|
||||
processQueue(handlers, completed);
|
||||
|
||||
// Free progressHandlers array since we'll never issue progress events
|
||||
progressHandlers = handlers = undef;
|
||||
|
||||
return completed;
|
||||
};
|
||||
|
||||
return deferred;
|
||||
|
||||
/**
|
||||
* Wrapper to allow _then to be replaced safely
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @return {Promise} new Promise
|
||||
* @throws {Error} if any argument is not null, undefined, or a Function
|
||||
*/
|
||||
function then(callback, errback, progback) {
|
||||
return _then(callback, errback, progback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _resolve to be replaced
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
return _resolve(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _resolve to be replaced
|
||||
*/
|
||||
function promiseReject(err) {
|
||||
return _resolve(rejected(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _progress to be replaced
|
||||
* @param {*} update progress update
|
||||
*/
|
||||
function promiseProgress(update) {
|
||||
return _progress(update);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if promiseOrValue is a promise or not. Uses the feature
|
||||
* test from http://wiki.commonjs.org/wiki/Promises/A to determine if
|
||||
* promiseOrValue is a promise.
|
||||
*
|
||||
* @param {*} promiseOrValue anything
|
||||
* @returns {Boolean} true if promiseOrValue is a {@link Promise}
|
||||
*/
|
||||
function isPromise(promiseOrValue) {
|
||||
return promiseOrValue && typeof promiseOrValue.then === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a competitive race, returning a promise that will resolve when
|
||||
* howMany of the supplied promisesOrValues have resolved, or will reject when
|
||||
* it becomes impossible for howMany to resolve, for example, when
|
||||
* (promisesOrValues.length - howMany) + 1 input promises reject.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promisesOrValues {Array} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param howMany {Number} number of promisesOrValues to resolve
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @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, callback, errback, progback) {
|
||||
|
||||
checkCallbacks(2, arguments);
|
||||
|
||||
return when(promisesOrValues, function(promisesOrValues) {
|
||||
|
||||
var toResolve, toReject, values, reasons, deferred, fulfillOne, rejectOne, progress, len, i;
|
||||
|
||||
len = promisesOrValues.length >>> 0;
|
||||
|
||||
toResolve = Math.max(0, Math.min(howMany, len));
|
||||
values = [];
|
||||
|
||||
toReject = (len - toResolve) + 1;
|
||||
reasons = [];
|
||||
|
||||
deferred = defer();
|
||||
|
||||
// No items in the input, resolve immediately
|
||||
if (!toResolve) {
|
||||
deferred.resolve(values);
|
||||
|
||||
} else {
|
||||
progress = deferred.progress;
|
||||
|
||||
rejectOne = function(reason) {
|
||||
reasons.push(reason);
|
||||
if(!--toReject) {
|
||||
fulfillOne = rejectOne = noop;
|
||||
deferred.reject(reasons);
|
||||
}
|
||||
};
|
||||
|
||||
fulfillOne = function(val) {
|
||||
// This orders the values based on promise resolution order
|
||||
// Another strategy would be to use the original position of
|
||||
// the corresponding promise.
|
||||
values.push(val);
|
||||
|
||||
if (!--toResolve) {
|
||||
fulfillOne = rejectOne = noop;
|
||||
deferred.resolve(values);
|
||||
}
|
||||
};
|
||||
|
||||
for(i = 0; i < len; ++i) {
|
||||
if(i in promisesOrValues) {
|
||||
when(promisesOrValues[i], fulfiller, rejecter, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.then(callback, errback, progback);
|
||||
|
||||
function rejecter(reason) {
|
||||
rejectOne(reason);
|
||||
}
|
||||
|
||||
function fulfiller(val) {
|
||||
fulfillOne(val);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a competitive race, returning a promise that will resolve when
|
||||
* any one of the supplied promisesOrValues has resolved or will reject when
|
||||
* *all* promisesOrValues have rejected.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @returns {Promise} promise that will resolve to the value that resolved first, or
|
||||
* will reject with an array of all rejected inputs.
|
||||
*/
|
||||
function any(promisesOrValues, callback, errback, progback) {
|
||||
|
||||
function unwrapSingleResult(val) {
|
||||
return callback ? callback(val[0]) : val[0];
|
||||
}
|
||||
|
||||
return some(promisesOrValues, 1, unwrapSingleResult, errback, progback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that will resolve only once all the supplied promisesOrValues
|
||||
* have resolved. The resolution value of the returned promise will be an array
|
||||
* containing the resolution values of each of the promisesOrValues.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param [callback] {Function}
|
||||
* @param [errback] {Function}
|
||||
* @param [progressHandler] {Function}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function all(promisesOrValues, callback, errback, progressHandler) {
|
||||
checkCallbacks(1, arguments);
|
||||
return map(promisesOrValues, identity).then(callback, errback, progressHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple promises into a single returned promise.
|
||||
* @memberOf when
|
||||
* @param {Promise|*} [...promises] two or more promises to join
|
||||
* @return {Promise} a promise that will fulfill when *all* the input promises
|
||||
* have fulfilled, or will reject when *any one* of the input promises rejects.
|
||||
*/
|
||||
function join(/* ...promises */) {
|
||||
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}
|
||||
*
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promise {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param mapFunc {Function} 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.
|
||||
*/
|
||||
function map(promise, mapFunc) {
|
||||
return when(promise, function(array) {
|
||||
var results, len, toResolve, resolve, reject, i, d;
|
||||
|
||||
// Since we know the resulting length, we can preallocate the results
|
||||
// array to avoid array expansions.
|
||||
toResolve = len = array.length >>> 0;
|
||||
results = [];
|
||||
d = defer();
|
||||
|
||||
if(!toResolve) {
|
||||
d.resolve(results);
|
||||
} else {
|
||||
|
||||
reject = d.reject;
|
||||
resolve = function resolveOne(item, i) {
|
||||
when(item, mapFunc).then(function(mapped) {
|
||||
results[i] = mapped;
|
||||
|
||||
if(!--toResolve) {
|
||||
d.resolve(results);
|
||||
}
|
||||
}, reject);
|
||||
};
|
||||
|
||||
// Since mapFunc may be async, get all invocations of it into flight
|
||||
for(i = 0; i < len; i++) {
|
||||
if(i in array) {
|
||||
resolve(array[i], i);
|
||||
} else {
|
||||
--toResolve;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return d.promise;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
|
||||
* input may contain {@link Promise}s and/or values, and reduceFunc
|
||||
* may return either a value or a {@link Promise}, *and* initialValue may
|
||||
* be a {@link Promise} for the starting value.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promise {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values. May also be a {@link Promise} for
|
||||
* an array.
|
||||
* @param reduceFunc {Function} reduce function reduce(currentValue, nextValue, index, total),
|
||||
* where total is the total number of items being reduced, and will be the same
|
||||
* in each call to reduceFunc.
|
||||
* @param [initialValue] {*} starting value, or a {@link Promise} for the starting value
|
||||
* @returns {Promise} that will resolve to the final reduced value
|
||||
*/
|
||||
function reduce(promise, reduceFunc /*, initialValue */) {
|
||||
var args = slice.call(arguments, 1);
|
||||
|
||||
return when(promise, function(array) {
|
||||
var total;
|
||||
|
||||
total = array.length;
|
||||
|
||||
// Wrap the supplied reduceFunc with one that handles promises and then
|
||||
// delegates to the supplied.
|
||||
args[0] = function (current, val, i) {
|
||||
return when(current, function (c) {
|
||||
return when(val, function (value) {
|
||||
return reduceFunc(c, value, i, total);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return reduceArray.apply(array, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that resolution of promiseOrValue will complete resolver with the completion
|
||||
* value of promiseOrValue, or instead with resolveValue if it is provided.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue
|
||||
* @param resolver {Resolver}
|
||||
* @param [resolveValue] anything
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function chain(promiseOrValue, resolver, resolveValue) {
|
||||
var useResolveValue = arguments.length > 2;
|
||||
|
||||
return when(promiseOrValue,
|
||||
function(val) {
|
||||
return resolver.resolve(useResolveValue ? resolveValue : val);
|
||||
},
|
||||
resolver.reject,
|
||||
resolver.progress
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Utility functions
|
||||
//
|
||||
|
||||
function processQueue(queue, value) {
|
||||
var handler, i = 0;
|
||||
|
||||
while (handler = queue[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that checks arrayOfCallbacks to ensure that each element is either
|
||||
* a function, or null or undefined.
|
||||
* @private
|
||||
*
|
||||
* @param arrayOfCallbacks {Array} array to check
|
||||
* @throws {Error} if any element of arrayOfCallbacks is something other than
|
||||
* a Functions, null, or undefined.
|
||||
*/
|
||||
function checkCallbacks(start, arrayOfCallbacks) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No-Op function used in method replacement
|
||||
* @private
|
||||
*/
|
||||
function noop() {}
|
||||
|
||||
slice = [].slice;
|
||||
|
||||
// ES5 reduce implementation if native not available
|
||||
// See: http://es5.github.com/#x15.4.4.21 as there are many
|
||||
// specifics and edge cases.
|
||||
reduceArray = [].reduce ||
|
||||
function(reduceFunc /*, initialValue */) {
|
||||
/*jshint maxcomplexity: 7*/
|
||||
|
||||
// ES5 dictates that reduce.length === 1
|
||||
|
||||
// This implementation deviates from ES5 spec in the following ways:
|
||||
// 1. It does not check if reduceFunc is a Callable
|
||||
|
||||
var arr, args, reduced, len, i;
|
||||
|
||||
i = 0;
|
||||
// This generates a jshint warning, despite being valid
|
||||
// "Missing 'new' prefix when invoking a constructor."
|
||||
// See https://github.com/jshint/jshint/issues/392
|
||||
arr = Object(this);
|
||||
len = arr.length >>> 0;
|
||||
args = arguments;
|
||||
|
||||
// If no initialValue, use first item of array (we know length !== 0 here)
|
||||
// and adjust i to start at second item
|
||||
if(args.length <= 1) {
|
||||
// Skip to the first real element in the array
|
||||
for(;;) {
|
||||
if(i in arr) {
|
||||
reduced = arr[i++];
|
||||
break;
|
||||
}
|
||||
|
||||
// If we reached the end of the array without finding any real
|
||||
// elements, it's a TypeError
|
||||
if(++i >= len) {
|
||||
throw new TypeError();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If initialValue provided, use it
|
||||
reduced = args[1];
|
||||
}
|
||||
|
||||
// Do the actual reduce
|
||||
for(;i < len; ++i) {
|
||||
// Skip holes
|
||||
if(i in arr) {
|
||||
reduced = reduceFunc(reduced, arr[i], i, arr);
|
||||
}
|
||||
}
|
||||
|
||||
return reduced;
|
||||
};
|
||||
|
||||
function identity(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
return when;
|
||||
});
|
||||
})(typeof define == 'function' && define.amd
|
||||
? define
|
||||
: function (deps, factory) { typeof exports === 'object'
|
||||
? (module.exports = factory())
|
||||
: (this.when = factory());
|
||||
}
|
||||
// Boilerplate for AMD, Node, and browser global
|
||||
);
|
||||
278
js/src/mopidy.js
Normal file
278
js/src/mopidy.js
Normal file
@ -0,0 +1,278 @@
|
||||
/*global bane:false, when:false*/
|
||||
|
||||
function Mopidy(settings) {
|
||||
this._settings = this._configure(settings || {});
|
||||
this._console = this._getConsole();
|
||||
|
||||
this._backoffDelay = this._settings.backoffDelayMin;
|
||||
this._pendingRequests = {};
|
||||
this._webSocket = null;
|
||||
|
||||
bane.createEventEmitter(this);
|
||||
this._delegateEvents();
|
||||
|
||||
if (this._settings.autoConnect) {
|
||||
this._connect();
|
||||
}
|
||||
}
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
settings.webSocketUrl = settings.webSocketUrl ||
|
||||
"ws://" + document.location.host + "/mopidy/ws/";
|
||||
|
||||
if (settings.autoConnect !== false) {
|
||||
settings.autoConnect = true;
|
||||
}
|
||||
|
||||
settings.backoffDelayMin = settings.backoffDelayMin || 1000;
|
||||
settings.backoffDelayMax = settings.backoffDelayMax || 64000;
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
Mopidy.prototype._getConsole = function () {
|
||||
var console = window.console || {};
|
||||
|
||||
console.log = console.log || function () {};
|
||||
console.warn = console.warn || function () {};
|
||||
console.error = console.error || function () {};
|
||||
|
||||
return console;
|
||||
};
|
||||
|
||||
Mopidy.prototype._delegateEvents = function () {
|
||||
// Remove existing event handlers
|
||||
this.off("websocket:close");
|
||||
this.off("websocket:error");
|
||||
this.off("websocket:incomingMessage");
|
||||
this.off("websocket:open");
|
||||
this.off("state:offline");
|
||||
|
||||
// Register basic set of event handlers
|
||||
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 === WebSocket.OPEN) {
|
||||
return;
|
||||
} else {
|
||||
this._webSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
this._webSocket = this._settings.webSocket ||
|
||||
new WebSocket(this._settings.webSocketUrl);
|
||||
|
||||
this._webSocket.onclose = function (close) {
|
||||
this.emit("websocket:close", close);
|
||||
}.bind(this);
|
||||
|
||||
this._webSocket.onerror = function (error) {
|
||||
this.emit("websocket:error", error);
|
||||
}.bind(this);
|
||||
|
||||
this._webSocket.onopen = function () {
|
||||
this.emit("websocket:open");
|
||||
}.bind(this);
|
||||
|
||||
this._webSocket.onmessage = function (message) {
|
||||
this.emit("websocket:incomingMessage", message);
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
Mopidy.prototype._cleanup = function (closeEvent) {
|
||||
Object.keys(this._pendingRequests).forEach(function (requestId) {
|
||||
var resolver = this._pendingRequests[requestId];
|
||||
delete this._pendingRequests[requestId];
|
||||
resolver.reject({
|
||||
message: "WebSocket closed",
|
||||
closeEvent: closeEvent
|
||||
});
|
||||
}.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 = this._backoffDelay * 2;
|
||||
if (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 (error) {
|
||||
this._console.warn("WebSocket error:", error.stack || error);
|
||||
};
|
||||
|
||||
Mopidy.prototype._send = function (message) {
|
||||
var deferred = when.defer();
|
||||
|
||||
switch (this._webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is still connecting"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closing"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closed"
|
||||
});
|
||||
break;
|
||||
default:
|
||||
message.jsonrpc = "2.0";
|
||||
message.id = this._nextRequestId();
|
||||
this._pendingRequests[message.id] = deferred.resolver;
|
||||
this._webSocket.send(JSON.stringify(message));
|
||||
this.emit("websocket:outgoingMessage", message);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
Mopidy.prototype._nextRequestId = (function () {
|
||||
var lastUsed = -1;
|
||||
return function () {
|
||||
lastUsed += 1;
|
||||
return lastUsed;
|
||||
};
|
||||
}());
|
||||
|
||||
Mopidy.prototype._handleMessage = function (message) {
|
||||
try {
|
||||
var data = JSON.parse(message.data);
|
||||
if (data.hasOwnProperty("id")) {
|
||||
this._handleResponse(data);
|
||||
} else if (data.hasOwnProperty("event")) {
|
||||
this._handleEvent(data);
|
||||
} else {
|
||||
this._console.warn(
|
||||
"Unknown message type received. Message was: " +
|
||||
message.data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
this._console.warn(
|
||||
"WebSocket message parsing failed. Message was: " +
|
||||
message.data);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Mopidy.prototype._handleResponse = function (responseMessage) {
|
||||
if (!this._pendingRequests.hasOwnProperty(responseMessage.id)) {
|
||||
this._console.warn(
|
||||
"Unexpected response received. Message was:", responseMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
var resolver = this._pendingRequests[responseMessage.id];
|
||||
delete this._pendingRequests[responseMessage.id];
|
||||
|
||||
if (responseMessage.hasOwnProperty("result")) {
|
||||
resolver.resolve(responseMessage.result);
|
||||
} else if (responseMessage.hasOwnProperty("error")) {
|
||||
resolver.reject(responseMessage.error);
|
||||
this._console.warn("Server returned error:", responseMessage.error);
|
||||
} else {
|
||||
resolver.reject({
|
||||
message: "Response without 'result' or 'error' received",
|
||||
data: {response: responseMessage}
|
||||
});
|
||||
this._console.warn(
|
||||
"Response without 'result' or 'error' received. Message was:",
|
||||
responseMessage);
|
||||
}
|
||||
};
|
||||
|
||||
Mopidy.prototype._handleEvent = function (eventMessage) {
|
||||
var type = eventMessage.event;
|
||||
var data = eventMessage;
|
||||
delete data.event;
|
||||
|
||||
this.emit("event:" + this._snakeToCamel(type), data);
|
||||
};
|
||||
|
||||
Mopidy.prototype._getApiSpec = function () {
|
||||
this._send({method: "core.describe"})
|
||||
.then(this._createApi.bind(this), this._handleWebSocketError)
|
||||
.then(null, this._handleWebSocketError);
|
||||
};
|
||||
|
||||
Mopidy.prototype._createApi = function (methods) {
|
||||
var caller = function (method) {
|
||||
return function () {
|
||||
var params = Array.prototype.slice.call(arguments);
|
||||
return this._send({
|
||||
method: method,
|
||||
params: params
|
||||
});
|
||||
}.bind(this);
|
||||
}.bind(this);
|
||||
|
||||
var getPath = function (fullName) {
|
||||
var path = fullName.split(".");
|
||||
if (path.length >= 1 && path[0] === "core") {
|
||||
path = path.slice(1);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
var createObjects = function (objPath) {
|
||||
var parentObj = this;
|
||||
objPath.forEach(function (objName) {
|
||||
objName = this._snakeToCamel(objName);
|
||||
parentObj[objName] = parentObj[objName] || {};
|
||||
parentObj = parentObj[objName];
|
||||
}.bind(this));
|
||||
return parentObj;
|
||||
}.bind(this);
|
||||
|
||||
var createMethod = function (fullMethodName) {
|
||||
var methodPath = getPath(fullMethodName);
|
||||
var methodName = this._snakeToCamel(methodPath.slice(-1)[0]);
|
||||
var object = createObjects(methodPath.slice(0, -1));
|
||||
object[methodName] = caller(fullMethodName);
|
||||
object[methodName].description = methods[fullMethodName].description;
|
||||
object[methodName].params = methods[fullMethodName].params;
|
||||
}.bind(this);
|
||||
|
||||
Object.keys(methods).forEach(createMethod);
|
||||
this.emit("state:online");
|
||||
};
|
||||
|
||||
Mopidy.prototype._snakeToCamel = function (name) {
|
||||
return name.replace(/(_[a-z])/g, function (match) {
|
||||
return match.toUpperCase().replace("_", "");
|
||||
});
|
||||
};
|
||||
29
js/test/bind-helper.js
Normal file
29
js/test/bind-helper.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* PhantomJS 1.6 does not support Function.prototype.bind, so we polyfill it.
|
||||
*
|
||||
* Implementation from:
|
||||
* https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
|
||||
*/
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function (oThis) {
|
||||
if (typeof this !== "function") {
|
||||
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
||||
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
|
||||
}
|
||||
|
||||
var aArgs = Array.prototype.slice.call(arguments, 1),
|
||||
fToBind = this,
|
||||
fNOP = function () {},
|
||||
fBound = function () {
|
||||
return fToBind.apply(this instanceof fNOP && oThis
|
||||
? this
|
||||
: oThis,
|
||||
aArgs.concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
|
||||
fNOP.prototype = this.prototype;
|
||||
fBound.prototype = new fNOP();
|
||||
|
||||
return fBound;
|
||||
};
|
||||
}
|
||||
669
js/test/mopidy-test.js
Normal file
669
js/test/mopidy-test.js
Normal file
@ -0,0 +1,669 @@
|
||||
/*global buster:false, assert:false, refute:false, when:false, Mopidy:false*/
|
||||
|
||||
buster.testCase("Mopidy", {
|
||||
setUp: function () {
|
||||
// Sinon.JS doesn't manage to stub PhantomJS' WebSocket implementation,
|
||||
// so we replace it with a dummy temporarily.
|
||||
var fakeWebSocket = function () {
|
||||
return {
|
||||
send: function () {},
|
||||
close: function () {}
|
||||
};
|
||||
};
|
||||
fakeWebSocket.CONNECTING = 0;
|
||||
fakeWebSocket.OPEN = 1;
|
||||
fakeWebSocket.CLOSING = 2;
|
||||
fakeWebSocket.CLOSED = 3;
|
||||
this.realWebSocket = WebSocket;
|
||||
window.WebSocket = fakeWebSocket;
|
||||
|
||||
this.webSocketConstructorStub = this.stub(window, "WebSocket");
|
||||
|
||||
this.webSocket = {
|
||||
close: this.stub(),
|
||||
send: this.stub()
|
||||
};
|
||||
this.mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
},
|
||||
|
||||
tearDown: function () {
|
||||
window.WebSocket = this.realWebSocket;
|
||||
},
|
||||
|
||||
"constructor": {
|
||||
"connects when autoConnect is true": function () {
|
||||
new Mopidy({autoConnect: true});
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + document.location.host + "/mopidy/ws/");
|
||||
},
|
||||
|
||||
"does not connect when autoConnect is false": function () {
|
||||
new Mopidy({autoConnect: false});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
},
|
||||
|
||||
"does not connect when passed a WebSocket": function () {
|
||||
new Mopidy({webSocket: {}});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
}
|
||||
},
|
||||
|
||||
"._connect": {
|
||||
"does nothing when the WebSocket is open": function () {
|
||||
this.webSocket.readyState = WebSocket.OPEN;
|
||||
var mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
|
||||
mopidy._connect();
|
||||
|
||||
refute.called(this.webSocket.close);
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
}
|
||||
},
|
||||
|
||||
"WebSocket events": {
|
||||
"emits 'websocket:close' when connection is closed": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.off("websocket:close");
|
||||
this.mopidy.on("websocket:close", spy);
|
||||
|
||||
var closeEvent = {};
|
||||
this.webSocket.onclose(closeEvent);
|
||||
|
||||
assert.calledOnceWith(spy, closeEvent);
|
||||
},
|
||||
|
||||
"emits 'websocket:error' when errors occurs": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.off("websocket:error");
|
||||
this.mopidy.on("websocket:error", spy);
|
||||
|
||||
var errorEvent = {};
|
||||
this.webSocket.onerror(errorEvent);
|
||||
|
||||
assert.calledOnceWith(spy, errorEvent);
|
||||
},
|
||||
|
||||
"emits 'websocket:incomingMessage' when a message arrives": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.off("websocket:incomingMessage");
|
||||
this.mopidy.on("websocket:incomingMessage", spy);
|
||||
|
||||
var messageEvent = {data: "this is a message"};
|
||||
this.webSocket.onmessage(messageEvent);
|
||||
|
||||
assert.calledOnceWith(spy, messageEvent);
|
||||
},
|
||||
|
||||
"emits 'websocket:open' when connection is opened": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.off("websocket:open");
|
||||
this.mopidy.on("websocket:open", spy);
|
||||
|
||||
this.webSocket.onopen();
|
||||
|
||||
assert.calledOnceWith(spy);
|
||||
}
|
||||
},
|
||||
|
||||
"._cleanup": {
|
||||
setUp: function () {
|
||||
this.mopidy.off("state:offline");
|
||||
},
|
||||
|
||||
"is called on 'websocket:close' event": function () {
|
||||
var closeEvent = {};
|
||||
var stub = this.stub(this.mopidy, "_cleanup");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("websocket:close", closeEvent);
|
||||
|
||||
assert.calledOnceWith(stub, closeEvent);
|
||||
},
|
||||
|
||||
"rejects all pending requests": function (done) {
|
||||
var closeEvent = {};
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
|
||||
var promise1 = this.mopidy._send({method: "foo"});
|
||||
var promise2 = this.mopidy._send({method: "bar"});
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 2);
|
||||
|
||||
this.mopidy._cleanup(closeEvent);
|
||||
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
when.join(promise1, promise2).then(done(function () {
|
||||
assert(false, "Promises should be rejected");
|
||||
}), done(function (error) {
|
||||
assert.equals(error.message, "WebSocket closed");
|
||||
assert.same(error.closeEvent, closeEvent);
|
||||
}));
|
||||
},
|
||||
|
||||
"emits 'state:offline' event when done": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.on("state:offline", spy);
|
||||
|
||||
this.mopidy._cleanup({});
|
||||
|
||||
assert.calledOnceWith(spy);
|
||||
}
|
||||
},
|
||||
|
||||
"._reconnect": {
|
||||
"is called when the state changes to offline": function () {
|
||||
var stub = this.stub(this.mopidy, "_reconnect");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("state:offline");
|
||||
|
||||
assert.calledOnceWith(stub);
|
||||
},
|
||||
|
||||
"tries to connect after an increasing backoff delay": function () {
|
||||
var clock = this.useFakeTimers();
|
||||
var connectStub = this.stub(this.mopidy, "_connect");
|
||||
var pendingSpy = this.spy();
|
||||
this.mopidy.on("reconnectionPending", pendingSpy);
|
||||
var reconnectingSpy = this.spy();
|
||||
this.mopidy.on("reconnecting", reconnectingSpy);
|
||||
|
||||
refute.called(connectStub);
|
||||
|
||||
this.mopidy._reconnect();
|
||||
assert.calledOnceWith(pendingSpy, {timeToAttempt: 1000});
|
||||
clock.tick(0);
|
||||
refute.called(connectStub);
|
||||
clock.tick(1000);
|
||||
assert.calledOnceWith(reconnectingSpy);
|
||||
assert.calledOnce(connectStub);
|
||||
|
||||
pendingSpy.reset();
|
||||
reconnectingSpy.reset();
|
||||
this.mopidy._reconnect();
|
||||
assert.calledOnceWith(pendingSpy, {timeToAttempt: 2000});
|
||||
assert.calledOnce(connectStub);
|
||||
clock.tick(0);
|
||||
assert.calledOnce(connectStub);
|
||||
clock.tick(1000);
|
||||
assert.calledOnce(connectStub);
|
||||
clock.tick(1000);
|
||||
assert.calledOnceWith(reconnectingSpy);
|
||||
assert.calledTwice(connectStub);
|
||||
|
||||
pendingSpy.reset();
|
||||
reconnectingSpy.reset();
|
||||
this.mopidy._reconnect();
|
||||
assert.calledOnceWith(pendingSpy, {timeToAttempt: 4000});
|
||||
assert.calledTwice(connectStub);
|
||||
clock.tick(0);
|
||||
assert.calledTwice(connectStub);
|
||||
clock.tick(2000);
|
||||
assert.calledTwice(connectStub);
|
||||
clock.tick(2000);
|
||||
assert.calledOnceWith(reconnectingSpy);
|
||||
assert.calledThrice(connectStub);
|
||||
},
|
||||
|
||||
"tries to connect at least about once per minute": function () {
|
||||
var clock = this.useFakeTimers();
|
||||
var connectStub = this.stub(this.mopidy, "_connect");
|
||||
var pendingSpy = this.spy();
|
||||
this.mopidy.on("reconnectionPending", pendingSpy);
|
||||
this.mopidy._backoffDelay = this.mopidy._settings.backoffDelayMax;
|
||||
|
||||
refute.called(connectStub);
|
||||
|
||||
this.mopidy._reconnect();
|
||||
assert.calledOnceWith(pendingSpy, {timeToAttempt: 64000});
|
||||
clock.tick(0);
|
||||
refute.called(connectStub);
|
||||
clock.tick(64000);
|
||||
assert.calledOnce(connectStub);
|
||||
|
||||
pendingSpy.reset();
|
||||
this.mopidy._reconnect();
|
||||
assert.calledOnceWith(pendingSpy, {timeToAttempt: 64000});
|
||||
assert.calledOnce(connectStub);
|
||||
clock.tick(0);
|
||||
assert.calledOnce(connectStub);
|
||||
clock.tick(64000);
|
||||
assert.calledTwice(connectStub);
|
||||
}
|
||||
},
|
||||
|
||||
"._resetBackoffDelay": {
|
||||
"is called on 'websocket:open' event": function () {
|
||||
var stub = this.stub(this.mopidy, "_resetBackoffDelay");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("websocket:open");
|
||||
|
||||
assert.calledOnceWith(stub);
|
||||
},
|
||||
|
||||
"resets the backoff delay to the minimum value": function () {
|
||||
this.mopidy._backoffDelay = this.mopidy._backoffDelayMax;
|
||||
|
||||
this.mopidy._resetBackoffDelay();
|
||||
|
||||
assert.equals(this.mopidy._backoffDelay,
|
||||
this.mopidy._settings.backoffDelayMin);
|
||||
}
|
||||
},
|
||||
|
||||
"close": {
|
||||
"unregisters reconnection hooks": function () {
|
||||
this.stub(this.mopidy, "off");
|
||||
|
||||
this.mopidy.close();
|
||||
|
||||
assert.calledOnceWith(
|
||||
this.mopidy.off, "state:offline", this.mopidy._reconnect);
|
||||
},
|
||||
|
||||
"closes the WebSocket": function () {
|
||||
this.mopidy.close();
|
||||
|
||||
assert.calledOnceWith(this.mopidy._webSocket.close);
|
||||
}
|
||||
},
|
||||
|
||||
"._handleWebSocketError": {
|
||||
"is called on 'websocket:error' event": function () {
|
||||
var error = {};
|
||||
var stub = this.stub(this.mopidy, "_handleWebSocketError");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("websocket:error", error);
|
||||
|
||||
assert.calledOnceWith(stub, error);
|
||||
},
|
||||
|
||||
"without stack logs the error to the console": function () {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var error = {};
|
||||
|
||||
this.mopidy._handleWebSocketError(error);
|
||||
|
||||
assert.calledOnceWith(stub, "WebSocket error:", error);
|
||||
},
|
||||
|
||||
"with stack logs the error to the console": function () {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var error = {stack: "foo"};
|
||||
|
||||
this.mopidy._handleWebSocketError(error);
|
||||
|
||||
assert.calledOnceWith(stub, "WebSocket error:", error.stack);
|
||||
}
|
||||
},
|
||||
|
||||
"._send": {
|
||||
"adds JSON-RPC fields to the message": function () {
|
||||
this.stub(this.mopidy, "_nextRequestId").returns(1);
|
||||
var stub = this.stub(JSON, "stringify");
|
||||
|
||||
this.mopidy._send({method: "foo"});
|
||||
|
||||
assert.calledOnceWith(stub, {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "foo"
|
||||
});
|
||||
},
|
||||
|
||||
"adds a resolver to the pending requests queue": function () {
|
||||
this.stub(this.mopidy, "_nextRequestId").returns(1);
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
|
||||
this.mopidy._send({method: "foo"});
|
||||
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 1);
|
||||
assert.isFunction(this.mopidy._pendingRequests[1].resolve);
|
||||
},
|
||||
|
||||
"sends message on the WebSocket": function () {
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
|
||||
this.mopidy._send({method: "foo"});
|
||||
|
||||
assert.calledOnce(this.mopidy._webSocket.send);
|
||||
},
|
||||
|
||||
"emits a 'websocket:outgoingMessage' event": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.on("websocket:outgoingMessage", spy);
|
||||
this.stub(this.mopidy, "_nextRequestId").returns(1);
|
||||
|
||||
this.mopidy._send({method: "foo"});
|
||||
|
||||
assert.calledOnceWith(spy, {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "foo"
|
||||
});
|
||||
},
|
||||
|
||||
"immediately rejects request if CONNECTING": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CONNECTING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is still connecting");
|
||||
}));
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSING": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is closing");
|
||||
}));
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSED": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSED;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is closed");
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
"._nextRequestId": {
|
||||
"returns an ever increasing ID": function () {
|
||||
var base = this.mopidy._nextRequestId();
|
||||
assert.equals(this.mopidy._nextRequestId(), base + 1);
|
||||
assert.equals(this.mopidy._nextRequestId(), base + 2);
|
||||
assert.equals(this.mopidy._nextRequestId(), base + 3);
|
||||
}
|
||||
},
|
||||
|
||||
"._handleMessage": {
|
||||
"is called on 'websocket:incomingMessage' event": function () {
|
||||
var messageEvent = {};
|
||||
var stub = this.stub(this.mopidy, "_handleMessage");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("websocket:incomingMessage", messageEvent);
|
||||
|
||||
assert.calledOnceWith(stub, messageEvent);
|
||||
},
|
||||
|
||||
"passes JSON-RPC responses on to _handleResponse": function () {
|
||||
var stub = this.stub(this.mopidy, "_handleResponse");
|
||||
var message = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: null
|
||||
};
|
||||
var messageEvent = {data: JSON.stringify(message)};
|
||||
|
||||
this.mopidy._handleMessage(messageEvent);
|
||||
|
||||
assert.calledOnceWith(stub, message);
|
||||
},
|
||||
|
||||
"passes events on to _handleEvent": function () {
|
||||
var stub = this.stub(this.mopidy, "_handleEvent");
|
||||
var message = {
|
||||
event: "track_playback_started",
|
||||
track: {}
|
||||
};
|
||||
var messageEvent = {data: JSON.stringify(message)};
|
||||
|
||||
this.mopidy._handleMessage(messageEvent);
|
||||
|
||||
assert.calledOnceWith(stub, message);
|
||||
},
|
||||
|
||||
"logs unknown messages": function () {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var messageEvent = {data: JSON.stringify({foo: "bar"})};
|
||||
|
||||
this.mopidy._handleMessage(messageEvent);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Unknown message type received. Message was: " +
|
||||
messageEvent.data);
|
||||
},
|
||||
|
||||
"logs JSON parsing errors": function () {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var messageEvent = {data: "foobarbaz"};
|
||||
|
||||
this.mopidy._handleMessage(messageEvent);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"WebSocket message parsing failed. Message was: " +
|
||||
messageEvent.data);
|
||||
}
|
||||
},
|
||||
|
||||
"._handleResponse": {
|
||||
"logs unexpected responses": function () {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1337,
|
||||
result: null
|
||||
};
|
||||
|
||||
this.mopidy._handleResponse(responseMessage);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Unexpected response received. Message was:", responseMessage);
|
||||
},
|
||||
|
||||
"removes the matching request from the pending queue": function () {
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
this.mopidy._send({method: "bar"});
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 1);
|
||||
|
||||
this.mopidy._handleResponse({
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0],
|
||||
result: "baz"
|
||||
});
|
||||
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
},
|
||||
|
||||
"resolves requests which get results back": function (done) {
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseResult = {};
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0],
|
||||
result: responseResult
|
||||
};
|
||||
|
||||
this.mopidy._handleResponse(responseMessage);
|
||||
promise.then(done(function (result) {
|
||||
assert.equals(result, responseResult);
|
||||
}), done(function () {
|
||||
assert(false);
|
||||
}));
|
||||
},
|
||||
|
||||
"rejects and logs requests which get errors back": function (done) {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseError = {message: "Error", data: {}};
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0],
|
||||
error: responseError
|
||||
};
|
||||
|
||||
this.mopidy._handleResponse(responseMessage);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Server returned error:", responseError);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(error, responseError);
|
||||
}));
|
||||
},
|
||||
|
||||
"rejects and logs responses without result or error": function (done) {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0]
|
||||
};
|
||||
|
||||
this.mopidy._handleResponse(responseMessage);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Response without 'result' or 'error' received. Message was:",
|
||||
responseMessage);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message,
|
||||
"Response without 'result' or 'error' received");
|
||||
assert.equals(error.data.response, responseMessage);
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
"._handleEvent": {
|
||||
"emits server side even on Mopidy object": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.on(spy);
|
||||
var track = {};
|
||||
var message = {
|
||||
event: "track_playback_started",
|
||||
track: track
|
||||
};
|
||||
|
||||
this.mopidy._handleEvent(message);
|
||||
|
||||
assert.calledOnceWith(spy,
|
||||
"event:trackPlaybackStarted", {track: track});
|
||||
}
|
||||
},
|
||||
|
||||
"._getApiSpec": {
|
||||
"is called on 'websocket:open' event": function () {
|
||||
var stub = this.stub(this.mopidy, "_getApiSpec");
|
||||
this.mopidy._delegateEvents();
|
||||
|
||||
this.mopidy.emit("websocket:open");
|
||||
|
||||
assert.calledOnceWith(stub);
|
||||
},
|
||||
|
||||
"gets Api description from server and calls _createApi": function () {
|
||||
var methods = {};
|
||||
var sendStub = this.stub(this.mopidy, "_send");
|
||||
sendStub.returns(when.resolve(methods));
|
||||
var _createApiStub = this.stub(this.mopidy, "_createApi");
|
||||
|
||||
this.mopidy._getApiSpec();
|
||||
|
||||
assert.calledOnceWith(sendStub, {method: "core.describe"});
|
||||
assert.calledOnceWith(_createApiStub, methods);
|
||||
}
|
||||
},
|
||||
|
||||
"._createApi": {
|
||||
"can create an API with methods on the root object": function () {
|
||||
refute.defined(this.mopidy.hello);
|
||||
refute.defined(this.mopidy.hi);
|
||||
|
||||
this.mopidy._createApi({
|
||||
hello: {
|
||||
description: "Says hello",
|
||||
params: []
|
||||
},
|
||||
hi: {
|
||||
description: "Says hi",
|
||||
params: []
|
||||
}
|
||||
});
|
||||
|
||||
assert.isFunction(this.mopidy.hello);
|
||||
assert.equals(this.mopidy.hello.description, "Says hello");
|
||||
assert.equals(this.mopidy.hello.params, []);
|
||||
assert.isFunction(this.mopidy.hi);
|
||||
assert.equals(this.mopidy.hi.description, "Says hi");
|
||||
assert.equals(this.mopidy.hi.params, []);
|
||||
},
|
||||
|
||||
"can create an API with methods on a sub-object": function () {
|
||||
refute.defined(this.mopidy.hello);
|
||||
|
||||
this.mopidy._createApi({
|
||||
"hello.world": {
|
||||
description: "Says hello to the world",
|
||||
params: []
|
||||
}
|
||||
});
|
||||
|
||||
assert.defined(this.mopidy.hello);
|
||||
assert.isFunction(this.mopidy.hello.world);
|
||||
},
|
||||
|
||||
"strips off 'core' from method paths": function () {
|
||||
refute.defined(this.mopidy.hello);
|
||||
|
||||
this.mopidy._createApi({
|
||||
"core.hello.world": {
|
||||
description: "Says hello to the world",
|
||||
params: []
|
||||
}
|
||||
});
|
||||
|
||||
assert.defined(this.mopidy.hello);
|
||||
assert.isFunction(this.mopidy.hello.world);
|
||||
},
|
||||
|
||||
"converts snake_case to camelCase": function () {
|
||||
refute.defined(this.mopidy.mightyGreetings);
|
||||
|
||||
this.mopidy._createApi({
|
||||
"mighty_greetings.hello_world": {
|
||||
description: "Says hello to the world",
|
||||
params: []
|
||||
}
|
||||
});
|
||||
|
||||
assert.defined(this.mopidy.mightyGreetings);
|
||||
assert.isFunction(this.mopidy.mightyGreetings.helloWorld);
|
||||
},
|
||||
|
||||
"triggers 'state:online' event when API is ready for use": function () {
|
||||
var spy = this.spy();
|
||||
this.mopidy.on("state:online", spy);
|
||||
|
||||
this.mopidy._createApi({});
|
||||
|
||||
assert.calledOnceWith(spy);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -115,17 +115,335 @@ Example JSON-RPC request::
|
||||
|
||||
Example JSON-RPC response::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", ...}}
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", "...": "..."}}
|
||||
|
||||
The JSON-RPC method ``core.describe`` returns a data structure describing all
|
||||
available methods. If you're unsure how the core API maps to JSON-RPC, having a
|
||||
look at the ``core.describe`` response can be helpful.
|
||||
|
||||
JavaScript wrapper
|
||||
==================
|
||||
|
||||
A JavaScript library wrapping the JSON-RPC over WebSocket API is under
|
||||
development. Details on it will appear here when it's released.
|
||||
Mopidy.js JavaScript library
|
||||
============================
|
||||
|
||||
We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets
|
||||
you quickly started with working on your client instead of figuring out how to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting the library
|
||||
-------------------
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
Thus, if you use Mopidy to host your web client, like described above, you can
|
||||
load the latest version of Mopidy.js by adding the following script tag to your
|
||||
HTML file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
If you don't use Mopidy to host your web client, you can find the JS files in
|
||||
the Git repo at:
|
||||
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
|
||||
If you want to work on the Mopidy.js library itself, you'll find a complete
|
||||
development setup in the ``js/`` dir in our repo. The instructions in
|
||||
``js/README.rst`` will guide you on your way.
|
||||
|
||||
|
||||
Creating an instance
|
||||
--------------------
|
||||
|
||||
Once you got Mopidy.js loaded, you need to create an instance of the wrapper:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy();
|
||||
|
||||
When you instantiate ``Mopidy()`` without arguments, it will connect to
|
||||
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
|
||||
your web client using Mopidy's web server, you'll need to pass the URL to the
|
||||
WebSocket end point:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy({
|
||||
webSocketUrl: "ws://localhost:6680/mopidy/ws/"
|
||||
});
|
||||
|
||||
|
||||
Hooking up to events
|
||||
--------------------
|
||||
|
||||
Once you have a Mopidy.js object, you can hook up to the events it emits. To
|
||||
explore your possibilities, it can be useful to subscribe to all events and log
|
||||
them:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on(console.log);
|
||||
|
||||
Several types of events are emitted:
|
||||
|
||||
- You can get notified about when the Mopidy.js object is connected to the
|
||||
server and ready for method calls, when it's offline, and when it's trying to
|
||||
reconnect to the server by looking at the events ``state:online``,
|
||||
``state:offline``, ``reconnectionPending``, and ``reconnecting``.
|
||||
|
||||
- You can get events sent from the Mopidy server by looking at the events with
|
||||
the name prefix ``event:``, like ``event:trackPlaybackStarted``.
|
||||
|
||||
- You can introspect what happens internally on the WebSocket by looking at the
|
||||
events emitted with the name prefix ``websocket:``.
|
||||
|
||||
Mopidy.js uses the event emitter library `BANE
|
||||
<https://github.com/busterjs/bane>`_, so you should refer to BANE's
|
||||
short API documentation to see how you can hook up your listeners to the
|
||||
different events.
|
||||
|
||||
|
||||
Calling core API methods
|
||||
------------------------
|
||||
|
||||
Once your Mopidy.js object has connected to the Mopidy server and emits the
|
||||
``state:online`` event, it is ready to accept core API method calls:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on("state:online", function () [
|
||||
mopidy.playback.next();
|
||||
});
|
||||
|
||||
Any calls you make before the ``state:online`` event is emitted will fail. If
|
||||
you've hooked up an errback (more on that a bit later) to the promise returned
|
||||
from the call, the errback will be called with an error message.
|
||||
|
||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
|
||||
API attributes is *not* available, but that shouldn't be a problem as we've
|
||||
added (undocumented) getters and setters for all of them, so you can access the
|
||||
attributes as well from JavaScript.
|
||||
|
||||
Both the WebSocket API and the JavaScript API are based on introspection of the
|
||||
core Python API. Thus, they will always be up to date and immediately reflect
|
||||
any changes we do to the core API.
|
||||
|
||||
The best way to explore the JavaScript API, is probably by opening your
|
||||
browser's console, and using its tab completion to navigate the API. You'll
|
||||
find the Mopidy core API exposed under ``mopidy.playback``,
|
||||
``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``.
|
||||
|
||||
All methods in the JavaScript API have an associated data structure describing
|
||||
the Python params it expects, and most methods also have the Python API
|
||||
documentation available. This is available right there in the browser console,
|
||||
by looking at the method's ``description`` and ``params`` attributes:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
console.log(mopidy.playback.next.params);
|
||||
console.log(mopidy.playback.next.description);
|
||||
|
||||
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
|
||||
by-name. Combinations of both, like we're used to from Python, isn't supported
|
||||
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
|
||||
passing parameters by-position.
|
||||
|
||||
Obviously, you'll want to get a return value from many of your method calls.
|
||||
Since everything is happening across the WebSocket and maybe even across the
|
||||
network, you'll get the results asynchronously. Instead of having to pass
|
||||
callbacks and errbacks to every method you call, the methods return "promise"
|
||||
objects, which you can use to pipe the future result as input to another
|
||||
method, or to hook up callback and errback functions.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var track = mopidy.playback.getCurrentTrack();
|
||||
// => ``track`` isn't a track, but a "promise" object
|
||||
|
||||
Instead, typical usage will look like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var printCurrentTrack = function (track) {
|
||||
if (track) {
|
||||
console.log("Currently playing:", track.name, "by",
|
||||
track.artists[0].name, "from", track.album.name);
|
||||
} else {
|
||||
console.log("No current track");
|
||||
}
|
||||
};
|
||||
|
||||
mopidy.playback.getCurrentTrack().then(printCurrentTrack, console.error);
|
||||
|
||||
The first function passed to ``then()``, ``printCurrentTrack``, is the callback
|
||||
that will be called if the method call succeeds. The second function,
|
||||
``console.error``, is the errback that will be called if anything goes wrong.
|
||||
If you don't hook up an errback, debugging will be hard as errors will silently
|
||||
go missing.
|
||||
|
||||
For debugging, you may be interested in errors from function without
|
||||
interesting return values as well. In that case, you can pass ``null`` as the
|
||||
callback:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.playback.next().then(null, console.error);
|
||||
|
||||
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||
implementation known as `when.js <https://github.com/cujojs/when>`_. Please
|
||||
refer to when.js' documentation or the standard for further details on how to
|
||||
work with promise objects.
|
||||
|
||||
|
||||
Cleaning up
|
||||
-----------
|
||||
|
||||
If you for some reason want to clean up after Mopidy.js before the web page is
|
||||
closed or navigated away from, you can close the WebSocket, unregister all
|
||||
event listeners, and delete the object like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
// Close the WebSocket without reconnecting. Letting the object be garbage
|
||||
// collected will have the same effect, so this isn't striclty necessary.
|
||||
mopidy.close();
|
||||
|
||||
// Unregister all event listeners. If you don't do this, you may have
|
||||
// lingering references to the object causing the garbage collector to not
|
||||
// clean up after it.
|
||||
mopidy.off();
|
||||
|
||||
// Delete your reference to the object, so it can be garbage collected.
|
||||
mopidy = null;
|
||||
|
||||
|
||||
Example to get started with
|
||||
---------------------------
|
||||
|
||||
1. Create an empty directory for your web client.
|
||||
|
||||
2. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point
|
||||
to your new directory.
|
||||
|
||||
3. Make sure that you've included
|
||||
``mopidy.frontends.http.HttpFrontend`` in
|
||||
:attr:`mopidy.settings.FRONTENDS`.
|
||||
|
||||
4. Start/restart Mopidy.
|
||||
|
||||
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
|
||||
world!".
|
||||
|
||||
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
|
||||
there.
|
||||
|
||||
7. Include Mopidy.js in your web page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
8. Add one of the following Mopidy.js examples of how to queue and start
|
||||
playback of your first playlist either to your web page or a JavaScript file
|
||||
that you include in your web page.
|
||||
|
||||
"Imperative" style:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var queueAndPlayFirstPlaylist = function () {
|
||||
mopidy.playlists.getPlaylists().then(function (playlists) {
|
||||
var playlist = playlists[0];
|
||||
console.log("Loading playlist:", playlist.name);
|
||||
mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) {
|
||||
mopidy.playback.play(tlTracks[0]).then(function () {
|
||||
mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
Approximately the same behavior in a more functional style, using chaining
|
||||
of promisies.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var getFirst = function (list) {
|
||||
return list[0];
|
||||
};
|
||||
|
||||
var extractTracks = function (playlist) {
|
||||
return playlist.tracks;
|
||||
};
|
||||
|
||||
var printTypeAndName = function (model) {
|
||||
console.log(model.__model__ + ": " + model.name);
|
||||
// By returning the playlist, this function can be inserted
|
||||
// anywhere a model with a name is piped in the chain.
|
||||
return model;
|
||||
};
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var printNowPlaying = function () {
|
||||
// By returning any arguments we get, the function can be inserted
|
||||
// anywhere in the chain.
|
||||
var args = arguments;
|
||||
return mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
return args;
|
||||
});
|
||||
};
|
||||
|
||||
var queueAndPlayFirstPlaylist = function () {
|
||||
mopidy.playlists.getPlaylists()
|
||||
// => list of Playlists
|
||||
.then(getFirst, console.error)
|
||||
// => Playlist
|
||||
.then(printTypeAndName, console.error)
|
||||
// => Playlist
|
||||
.then(extractTracks, console.error)
|
||||
// => list of Tracks
|
||||
.then(mopidy.tracklist.add, console.error)
|
||||
// => list of TlTracks
|
||||
.then(getFirst, console.error)
|
||||
// => TlTrack
|
||||
.then(mopidy.playback.play, console.error)
|
||||
// => null
|
||||
.then(printNowPlaying, console.error);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
9. The web page should now queue and play your first playlist every time your
|
||||
load it. See the browser's console for output from the function, any errors,
|
||||
and a all events that are emitted.
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
1187
mopidy/frontends/http/data/mopidy.js
Normal file
1187
mopidy/frontends/http/data/mopidy.js
Normal file
File diff suppressed because it is too large
Load Diff
5
mopidy/frontends/http/data/mopidy.min.js
vendored
Normal file
5
mopidy/frontends/http/data/mopidy.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user