Merge branch 'develop' into feature/library-browse
Conflicts: mopidy/backends/local/json/library.py mopidy/core/actor.py tests/backends/local/library_test.py
This commit is contained in:
commit
1fd1a38013
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ node_modules/
|
||||
nosetests.xml
|
||||
*~
|
||||
*.orig
|
||||
js/test/lib/
|
||||
|
||||
3
.mailmap
3
.mailmap
@ -11,3 +11,6 @@ Alexandre Petitjean <alpetitjean@gmail.com> <alexandre.petitjean@lne.fr>
|
||||
Javier Domingo Cansino <javierdo1@gmail.com> <javier.domingo@fon.com>
|
||||
Lasse Bigum <lasse@bigum.org> <l.bigum@samsung.com>
|
||||
Nick Steel <kingosticks@gmail.com> <kingosticks@gmail.com>
|
||||
Janez Troha <janez.troha@gmail.com> <dz0ny@users.noreply.github.com>
|
||||
Luke Giuliani <luke@giuliani.com.au>
|
||||
Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
|
||||
3
AUTHORS
3
AUTHORS
@ -30,3 +30,6 @@
|
||||
- Lasse Bigum <lasse@bigum.org>
|
||||
- David Eisner <david.eisner@oriel.oxon.org>
|
||||
- Pål Ruud <ruudud@gmail.com>
|
||||
- Paul Connolley <paul.connolley@gmail.com>
|
||||
- Luke Giuliani <luke@giuliani.com.au>
|
||||
- Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
|
||||
13
MANIFEST.in
13
MANIFEST.in
@ -1,12 +1,23 @@
|
||||
include *.py
|
||||
include *.rst
|
||||
include .coveragerc
|
||||
include .mailmap
|
||||
include .travis.yml
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include data/mopidy.desktop
|
||||
|
||||
recursive-include data *
|
||||
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
|
||||
recursive-include js *
|
||||
prune js/node_modules
|
||||
prune js/test/lib
|
||||
|
||||
recursive-include mopidy *.conf
|
||||
recursive-include mopidy/http/data *
|
||||
|
||||
recursive-include tests *.py
|
||||
recursive-include tests/data *
|
||||
|
||||
@ -129,7 +129,7 @@ After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Getting the library for development on the library
|
||||
|
||||
@ -7,6 +7,11 @@ This changelog is used to track all major changes to Mopidy.
|
||||
v0.18.0 (UNRELEASED)
|
||||
====================
|
||||
|
||||
**MPD frontend**
|
||||
|
||||
- Empty commands now return a ``ACK [5@0] {} No command given`` error instead
|
||||
of ``OK``. This is consistent with the original MPD server implementation.
|
||||
|
||||
**Core API**
|
||||
|
||||
- Expose :meth:`mopidy.core.Core.version` for HTTP clients to manage
|
||||
@ -15,6 +20,12 @@ v0.18.0 (UNRELEASED)
|
||||
- Add :class:`mopidy.models.Ref` class for use as a lightweight reference to
|
||||
other model types, containing just an URI, a name, and an object type.
|
||||
|
||||
**Extension registry**
|
||||
|
||||
- Switched to using a registry model for classes provided by extension. This
|
||||
allows extensions to be extended as needed for plugable local libraries.
|
||||
(Fixes :issue:`601`)
|
||||
|
||||
**Pluggable local libraries**
|
||||
|
||||
Fixes issues :issue:`44`, partially resolves :issue:`397`, and causes
|
||||
@ -33,10 +44,25 @@ a temporary regression of :issue:`527`.
|
||||
- Added support for deprecated config values in order to allow for
|
||||
graceful removal of :confval:`local/tag_cache_file`.
|
||||
|
||||
- Added :confval:`local/library` to select which library to use.
|
||||
|
||||
- Added :confval:`local/data_dir` to have a common setting for where to store
|
||||
local library data. This is intended to avoid every single local library
|
||||
provider having to have it's own setting for this.
|
||||
|
||||
- Added :confval:`local/scan_flush_threshold` to control how often to tell
|
||||
local libraries to store changes.
|
||||
|
||||
**Streaming backend**
|
||||
|
||||
- Live lookup of URI metadata has been added. (Fixes :issue:`540`)
|
||||
|
||||
**HTTP frontend**
|
||||
|
||||
- Upgrade Mopidy.js dependencies and add support for using Mopidy.js with
|
||||
Browserify. This version has been released to npm as Mopidy.js v0.2.0.
|
||||
(Fixes: :issue:`609`)
|
||||
|
||||
**Internal changes**
|
||||
|
||||
- Events from the audio actor, backends, and core actor are now emitted
|
||||
@ -414,7 +440,7 @@ A release with a number of small and medium fixes, with no specific focus.
|
||||
objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the
|
||||
``tlid`` field. (Fixes: :issue:`501`)
|
||||
|
||||
- Upgrade Mopidy.js dependencies. This version has been released to NPM as
|
||||
- Upgrade Mopidy.js dependencies. This version has been released to npm as
|
||||
Mopidy.js v0.1.1.
|
||||
|
||||
**Extension support**
|
||||
|
||||
@ -83,6 +83,10 @@ Additionally, extensions can provide extra commands. Run `mopidy --help`
|
||||
for a list of what is available on your system and command-specific help.
|
||||
Commands for disabled extensions will be listed, but can not be run.
|
||||
|
||||
.. cmdoption:: local clear
|
||||
|
||||
Clear local media files from the local library.
|
||||
|
||||
.. cmdoption:: local scan
|
||||
|
||||
Scan local media files present in your library.
|
||||
|
||||
@ -29,10 +29,20 @@ Configuration values
|
||||
|
||||
If the local extension should be enabled or not.
|
||||
|
||||
.. confval:: local/library
|
||||
|
||||
Local library provider to use, change this if you want to use a third party
|
||||
library for local files.
|
||||
|
||||
.. confval:: local/media_dir
|
||||
|
||||
Path to directory with local media files.
|
||||
|
||||
.. confval:: local/data_dir
|
||||
|
||||
Path to directory to store local metadata such as libraries and playlists
|
||||
in.
|
||||
|
||||
.. confval:: local/playlists_dir
|
||||
|
||||
Path to playlists directory with m3u files for local media.
|
||||
@ -42,6 +52,11 @@ Configuration values
|
||||
Number of milliseconds before giving up scanning a file and moving on to
|
||||
the next file.
|
||||
|
||||
.. confval:: local/scan_flush_threshold
|
||||
|
||||
Number of tracks to wait before telling library it should try and store
|
||||
its progress so far. Some libraries might not respect this setting.
|
||||
|
||||
.. confval:: local/excluded_file_extensions
|
||||
|
||||
File extensions to exclude when scanning the media directory. Values
|
||||
@ -84,34 +99,13 @@ Pluggable library support
|
||||
-------------------------
|
||||
|
||||
Local libraries are fully pluggable. What this means is that users may opt to
|
||||
disable the current default library ``local-json``, replacing it with a third
|
||||
disable the current default library ``json``, replacing it with a third
|
||||
party one. When running :command:`mopidy local scan` mopidy will populate
|
||||
whatever the current active library is with data. Only one library may be
|
||||
active at a time.
|
||||
|
||||
|
||||
*****************
|
||||
Mopidy-Local-JSON
|
||||
*****************
|
||||
|
||||
Extension for storing local music library in a JSON file, default built in
|
||||
library for local files.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/local/json/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: local-json/enabled
|
||||
|
||||
If the local-json extension should be enabled or not.
|
||||
|
||||
.. confval:: local-json/json_file
|
||||
|
||||
Path to a file to store the gzipped JSON data in.
|
||||
To create a new library provider you must create class that implements the
|
||||
:class:`~mopidy.backends.local.Libary` interface and install it in the
|
||||
extension registry under ``local:library``. Any data that the library needs
|
||||
to store on disc should be stored in :confval:`local/data_dir` using the
|
||||
library name as part of the filename or directory to avoid any conflicts.
|
||||
|
||||
@ -16,7 +16,7 @@ Glossary
|
||||
:term:`tracklist`. To use the core module, see :ref:`core-api`.
|
||||
|
||||
extension
|
||||
A Python package that can extend Mopidy with on or more
|
||||
A Python package that can extend Mopidy with one or more
|
||||
:term:`backends <backend>`, :term:`frontends <frontend>`, or GStreamer
|
||||
elements like :term:`mixers <mixer>`. See :ref:`ext` for a list of
|
||||
existing extensions and :ref:`extensiondev` for how to make a new
|
||||
|
||||
@ -42,6 +42,19 @@ in the same way as you get updates to the rest of your distribution.
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
Note that this will only install the main Mopidy package. For e.g. Spotify
|
||||
or SoundCloud support you need to install the respective extension packages.
|
||||
To list all the extensions available from apt.mopidy.com, you can run::
|
||||
|
||||
apt-cache search mopidy
|
||||
|
||||
To install one of the listed packages, e.g. ``mopidy-spotify``, simply run::
|
||||
|
||||
sudo apt-get install mopidy-spotify
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from apt.mopidy.com, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
@ -62,6 +62,19 @@ you a lot better performance.
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
Note that this will only install the main Mopidy package. For e.g. Spotify
|
||||
or SoundCloud support you need to install the respective extension packages.
|
||||
To list all the extensions available from apt.mopidy.com, you can run::
|
||||
|
||||
apt-cache search mopidy
|
||||
|
||||
To install one of the listed packages, e.g. ``mopidy-spotify``, simply run::
|
||||
|
||||
sudo apt-get install mopidy-spotify
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from apt.mopidy.com, see :ref:`ext`.
|
||||
|
||||
#. Since I have a HDMI cable connected, but want the sound on the analog sound
|
||||
connector, I have to run::
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ module.exports = function (grunt) {
|
||||
" * Licensed under the Apache License, Version 2.0 */\n",
|
||||
files: {
|
||||
own: ["Gruntfile.js", "src/**/*.js", "test/**/*-test.js"],
|
||||
main: "src/mopidy.js",
|
||||
concat: "../mopidy/http/data/mopidy.js",
|
||||
minified: "../mopidy/http/data/mopidy.min.js"
|
||||
}
|
||||
@ -18,19 +19,35 @@ module.exports = function (grunt) {
|
||||
buster: {
|
||||
all: {}
|
||||
},
|
||||
concat: {
|
||||
options: {
|
||||
banner: "<%= meta.banner %>",
|
||||
stripBanners: true
|
||||
},
|
||||
all: {
|
||||
browserify: {
|
||||
test_mopidy: {
|
||||
files: {
|
||||
"<%= meta.files.concat %>": [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js",
|
||||
"src/mopidy.js"
|
||||
]
|
||||
"test/lib/mopidy.js": "<%= meta.files.main %>"
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
},
|
||||
test_when: {
|
||||
files: {
|
||||
"test/lib/when.js": "node_modules/when/when.js"
|
||||
},
|
||||
options: {
|
||||
standalone: "when"
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
files: {
|
||||
"<%= meta.files.concat %>": "<%= meta.files.main %>"
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -70,12 +87,13 @@ module.exports = function (grunt) {
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask("test", ["jshint", "buster"]);
|
||||
grunt.registerTask("build", ["test", "concat", "uglify"]);
|
||||
grunt.registerTask("test_build", ["browserify:test_when", "browserify:test_mopidy"]);
|
||||
grunt.registerTask("test", ["jshint", "test_build", "buster"]);
|
||||
grunt.registerTask("build", ["test", "browserify:dist", "uglify"]);
|
||||
grunt.registerTask("default", ["build"]);
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
grunt.loadNpmTasks("grunt-contrib-concat");
|
||||
grunt.loadNpmTasks("grunt-browserify");
|
||||
grunt.loadNpmTasks("grunt-contrib-jshint");
|
||||
grunt.loadNpmTasks("grunt-contrib-uglify");
|
||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
||||
|
||||
25
js/README.md
25
js/README.md
@ -35,7 +35,7 @@ Mopidy.js using npm:
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Using the library
|
||||
@ -80,3 +80,26 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in
|
||||
`package.json` and thus isn't available through `npm run-script`:
|
||||
|
||||
PATH=./node_modules/.bin:$PATH grunt foo
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
### 0.2.0 (2014-01-04)
|
||||
|
||||
- **Backwards incompatible change for Node.js users:**
|
||||
`var Mopidy = require('mopidy').Mopidy;` must be changed to
|
||||
`var Mopidy = require('mopidy');`
|
||||
|
||||
- Add support for [Browserify](http://browserify.org/).
|
||||
|
||||
- Upgrade dependencies.
|
||||
|
||||
### 0.1.1 (2013-09-17)
|
||||
|
||||
- Upgrade dependencies.
|
||||
|
||||
### 0.1.0 (2013-03-31)
|
||||
|
||||
- Initial release as a Node.js module to the
|
||||
[npm registry](https://npmjs.org/).
|
||||
|
||||
12
js/buster.js
12
js/buster.js
@ -2,23 +2,13 @@ var config = module.exports;
|
||||
|
||||
config.browser_tests = {
|
||||
environment: "browser",
|
||||
libs: [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js"
|
||||
],
|
||||
sources: ["src/**/*.js"],
|
||||
libs: ["test/lib/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
};
|
||||
|
||||
config.node_tests = {
|
||||
environment: "node",
|
||||
libs: [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js"
|
||||
],
|
||||
sources: ["src/**/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
|
||||
@ -1,171 +0,0 @@
|
||||
/**
|
||||
* BANE - Browser globals, AMD and Node Events
|
||||
*
|
||||
* https://github.com/busterjs/bane
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
((typeof define === "function" && define.amd && function (m) { define("bane", 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 };
|
||||
});
|
||||
1
js/lib/websocket/browser.js
Normal file
1
js/lib/websocket/browser.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = { Client: window.WebSocket };
|
||||
4
js/lib/websocket/package.json
Normal file
4
js/lib/websocket/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"browser": "browser.js",
|
||||
"main": "server.js"
|
||||
}
|
||||
1
js/lib/websocket/server.js
Normal file
1
js/lib/websocket/server.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('faye-websocket');
|
||||
@ -1,922 +0,0 @@
|
||||
/** @license MIT License (c) copyright 2011-2013 original author or authors */
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @author Brian Cavalier
|
||||
* @author John Hann
|
||||
* @version 2.4.0
|
||||
*/
|
||||
(function(define, global) { 'use strict';
|
||||
define(function (require) {
|
||||
|
||||
// Public API
|
||||
|
||||
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 = isPromiseLike; // DEPRECATED: use isPromiseLike
|
||||
when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable
|
||||
|
||||
/**
|
||||
* Register an observer for a promise or immediate value.
|
||||
*
|
||||
* @param {*} promiseOrValue
|
||||
* @param {function?} [onFulfilled] callback to be called when promiseOrValue is
|
||||
* successfully fulfilled. If promiseOrValue is an immediate value, callback
|
||||
* will be invoked immediately.
|
||||
* @param {function?} [onRejected] callback to be called when promiseOrValue is
|
||||
* rejected.
|
||||
* @param {function?} [onProgress] 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, onFulfilled, onRejected, onProgress) {
|
||||
// Get a trusted promise for the input promiseOrValue, and then
|
||||
// register promise handlers
|
||||
return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted Promise constructor. A Promise created from this constructor is
|
||||
* 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(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
|
||||
* @return {Promise}
|
||||
*/
|
||||
otherwise: function(onRejected) {
|
||||
return this.then(undef, onRejected);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures that onFulfilledOrRejected will be called regardless of whether
|
||||
* this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT
|
||||
* receive the promises' value or reason. Any returned value will be disregarded.
|
||||
* onFulfilledOrRejected may throw or return a rejected promise to signal
|
||||
* an additional error.
|
||||
* @param {function} onFulfilledOrRejected handler to be called regardless of
|
||||
* fulfillment or rejection
|
||||
* @returns {Promise}
|
||||
*/
|
||||
ensure: function(onFulfilledOrRejected) {
|
||||
return this.then(injectHandler, injectHandler)['yield'](this);
|
||||
|
||||
function injectHandler() {
|
||||
return resolve(onFulfilledOrRejected());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(function() { return value; })
|
||||
* @param {*} value
|
||||
* @return {Promise} a promise that:
|
||||
* - is fulfilled if value is not a promise, or
|
||||
* - if value is a promise, will fulfill with its value, or reject
|
||||
* with its reason.
|
||||
*/
|
||||
'yield': function(value) {
|
||||
return this.then(function() {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* i.e. onFulfilled.apply(undefined, array).
|
||||
* @param {function} onFulfilled function to receive spread arguments
|
||||
* @return {Promise}
|
||||
*/
|
||||
spread: function(onFulfilled) {
|
||||
return this.then(function(array) {
|
||||
// array may contain promises, so resolve its contents.
|
||||
return all(array, function(array) {
|
||||
return onFulfilled.apply(undef, array);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected)
|
||||
* @deprecated
|
||||
*/
|
||||
always: function(onFulfilledOrRejected, onProgress) {
|
||||
return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a resolved promise. The returned promise will be
|
||||
* - fulfilled with promiseOrValue if it is a value, or
|
||||
* - if promiseOrValue is a promise
|
||||
* - fulfilled with promiseOrValue's value after it is fulfilled
|
||||
* - rejected with promiseOrValue's reason after it is rejected
|
||||
* @param {*} value
|
||||
* @return {Promise}
|
||||
*/
|
||||
function resolve(value) {
|
||||
return promise(function(resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rejected promise for the supplied promiseOrValue. The returned
|
||||
* promise will be rejected with:
|
||||
* - promiseOrValue, if it is a value, or
|
||||
* - if promiseOrValue is a promise
|
||||
* - promiseOrValue's value after it is fulfilled
|
||||
* - promiseOrValue's reason after it is rejected
|
||||
* @param {*} promiseOrValue the rejected value of the returned {@link Promise}
|
||||
* @return {Promise} rejected {@link Promise}
|
||||
*/
|
||||
function reject(promiseOrValue) {
|
||||
return when(promiseOrValue, rejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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,
|
||||
* notify: function:Promise
|
||||
* }}}
|
||||
*/
|
||||
function defer() {
|
||||
var deferred, pending, resolved;
|
||||
|
||||
// Optimize object shape
|
||||
deferred = {
|
||||
promise: undef, resolve: undef, reject: undef, notify: undef,
|
||||
resolver: { resolve: undef, reject: undef, notify: undef }
|
||||
};
|
||||
|
||||
deferred.promise = pending = promise(makeDeferred);
|
||||
|
||||
return deferred;
|
||||
|
||||
function makeDeferred(resolvePending, rejectPending, notifyPending) {
|
||||
deferred.resolve = deferred.resolver.resolve = function(value) {
|
||||
if(resolved) {
|
||||
return resolve(value);
|
||||
}
|
||||
resolved = true;
|
||||
resolvePending(value);
|
||||
return pending;
|
||||
};
|
||||
|
||||
deferred.reject = deferred.resolver.reject = function(reason) {
|
||||
if(resolved) {
|
||||
return resolve(rejected(reason));
|
||||
}
|
||||
resolved = true;
|
||||
rejectPending(reason);
|
||||
return pending;
|
||||
};
|
||||
|
||||
deferred.notify = deferred.resolver.notify = function(update) {
|
||||
notifyPending(update);
|
||||
return update;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise whose fate is determined by resolver.
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
*/
|
||||
function promise(resolver) {
|
||||
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 {
|
||||
resolver(promiseResolve, promiseReject, promiseNotify);
|
||||
} catch(e) {
|
||||
promiseReject(e);
|
||||
}
|
||||
|
||||
// Return the promise
|
||||
return self;
|
||||
|
||||
/**
|
||||
* 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 _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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the ultimate fulfillment or rejection
|
||||
* @param {*|Promise} val resolution value
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
if(!consumers) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = coerce(val);
|
||||
scheduleConsumers(consumers, value);
|
||||
consumers = undef;
|
||||
|
||||
if(status) {
|
||||
updateStatus(value, status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject this promise with the supplied reason, which will be used verbatim.
|
||||
* @param {*} reason reason for the rejection
|
||||
*/
|
||||
function promiseReject(reason) {
|
||||
promiseResolve(rejected(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @param {*} update progress event payload to pass to all listeners
|
||||
*/
|
||||
function promiseNotify(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 {*} 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) {
|
||||
return x;
|
||||
}
|
||||
|
||||
if (!(x === Object(x) && 'then' in x)) {
|
||||
return fulfilled(x);
|
||||
}
|
||||
|
||||
return promise(function(resolve, reject, notify) {
|
||||
enqueue(function() {
|
||||
try {
|
||||
// We must check and assimilate in the same tick, but not the
|
||||
// current tick, careful only to access promiseOrValue.then once.
|
||||
var untrustedThen = x.then;
|
||||
|
||||
if(typeof untrustedThen === 'function') {
|
||||
fcall(untrustedThen, x, resolve, reject, notify);
|
||||
} else {
|
||||
// It's a value, create a fulfilled wrapper
|
||||
resolve(fulfilled(x));
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// Something went wrong, reject
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for a near, fulfilled value
|
||||
* @param {*} value
|
||||
* @constructor
|
||||
*/
|
||||
function NearFulfilledProxy(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
NearFulfilledProxy.prototype.when = function(onResult) {
|
||||
return typeof onResult === 'function' ? onResult(this.value) : this.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy for a near rejection
|
||||
* @param {*} reason
|
||||
* @constructor
|
||||
*/
|
||||
function NearRejectedProxy(reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
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
|
||||
* in the next queue drain run.
|
||||
* @private
|
||||
* @param {Array} handlers queue of handlers to execute
|
||||
* @param {*} value passed as the only arg to each handler
|
||||
*/
|
||||
function scheduleConsumers(handlers, value) {
|
||||
enqueue(function() {
|
||||
var handler, i = 0;
|
||||
while (handler = handlers[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(value, status) {
|
||||
value.then(statusFulfilled, statusRejected);
|
||||
|
||||
function statusFulfilled() { status.fulfilled(); }
|
||||
function statusRejected(r) { status.rejected(r); }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 isPromiseLike(x) {
|
||||
return x && typeof x.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.
|
||||
*
|
||||
* @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] 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) {
|
||||
|
||||
return when(promisesOrValues, function(promisesOrValues) {
|
||||
|
||||
return promise(resolveSome).then(onFulfilled, onRejected, onProgress);
|
||||
|
||||
function resolveSome(resolve, reject, notify) {
|
||||
var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i;
|
||||
|
||||
len = promisesOrValues.length >>> 0;
|
||||
|
||||
toResolve = Math.max(0, Math.min(howMany, len));
|
||||
values = [];
|
||||
|
||||
toReject = (len - toResolve) + 1;
|
||||
reasons = [];
|
||||
|
||||
// No items in the input, resolve immediately
|
||||
if (!toResolve) {
|
||||
resolve(values);
|
||||
|
||||
} else {
|
||||
rejectOne = function(reason) {
|
||||
reasons.push(reason);
|
||||
if(!--toReject) {
|
||||
fulfillOne = rejectOne = identity;
|
||||
reject(reasons);
|
||||
}
|
||||
};
|
||||
|
||||
fulfillOne = function(val) {
|
||||
// This orders the values based on promise resolution order
|
||||
values.push(val);
|
||||
if (!--toResolve) {
|
||||
fulfillOne = rejectOne = identity;
|
||||
resolve(values);
|
||||
}
|
||||
};
|
||||
|
||||
for(i = 0; i < len; ++i) {
|
||||
if(i in promisesOrValues) {
|
||||
when(promisesOrValues[i], fulfiller, rejecter, notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @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.
|
||||
*/
|
||||
function any(promisesOrValues, onFulfilled, onRejected, onProgress) {
|
||||
|
||||
function unwrapSingleResult(val) {
|
||||
return onFulfilled ? onFulfilled(val[0]) : val[0];
|
||||
}
|
||||
|
||||
return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Array|Promise} promisesOrValues array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @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) {
|
||||
return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple promises into a single returned promise.
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
function resolveMap(resolve, reject, notify) {
|
||||
var results, len, toResolve, i;
|
||||
|
||||
// Since we know the resulting length, we can preallocate the results
|
||||
// array to avoid array expansions.
|
||||
toResolve = len = array.length >>> 0;
|
||||
results = [];
|
||||
|
||||
if(!toResolve) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
|
||||
* input may contain promises and/or values, and reduceFunc
|
||||
* may return either a value or a promise, *and* initialValue may
|
||||
* be a promise for the starting value.
|
||||
*
|
||||
* @param {Array|Promise} promise array or promise for an array of anything,
|
||||
* may contain a mix of promises and values.
|
||||
* @param {function} reduceFunc 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.
|
||||
* @returns {Promise} that will resolve to the final reduced value
|
||||
*/
|
||||
function reduce(promise, reduceFunc /*, initialValue */) {
|
||||
var args = fcall(slice, 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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' };
|
||||
}
|
||||
|
||||
//
|
||||
// Internals, utilities, etc.
|
||||
//
|
||||
|
||||
var reduceArray, slice, fcall, nextTick, handlerQueue,
|
||||
setTimeout, funcProto, call, arrayProto, monitorApi,
|
||||
cjsRequire, undef;
|
||||
|
||||
cjsRequire = require;
|
||||
|
||||
//
|
||||
// Shared handler queue processing
|
||||
//
|
||||
// Credit to Twisol (https://github.com/Twisol) for suggesting
|
||||
// this type of extensible queue + trampoline approach for
|
||||
// next-tick conflation.
|
||||
|
||||
handlerQueue = [];
|
||||
|
||||
/**
|
||||
* Enqueue a task. If the queue is not currently scheduled to be
|
||||
* drained, schedule it.
|
||||
* @param {function} task
|
||||
*/
|
||||
function enqueue(task) {
|
||||
if(handlerQueue.push(task) === 1) {
|
||||
nextTick(drainQueue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
var task, i = 0;
|
||||
|
||||
while(task = handlerQueue[i++]) {
|
||||
task();
|
||||
}
|
||||
|
||||
handlerQueue = [];
|
||||
}
|
||||
|
||||
// capture setTimeout to avoid being caught by fake timers
|
||||
// used in time based tests
|
||||
setTimeout = global.setTimeout;
|
||||
|
||||
// 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;
|
||||
call = funcProto.call;
|
||||
fcall = funcProto.bind
|
||||
? call.bind(call)
|
||||
: function(f, context) {
|
||||
return f.apply(context, slice.call(arguments, 2));
|
||||
};
|
||||
|
||||
// Safe array ops
|
||||
arrayProto = [];
|
||||
slice = arrayProto.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. 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
|
||||
reduceArray = arrayProto.reduce ||
|
||||
function(reduceFunc /*, initialValue */) {
|
||||
/*jshint maxcomplexity: 7*/
|
||||
var arr, args, reduced, len, i;
|
||||
|
||||
i = 0;
|
||||
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) {
|
||||
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 (factory) { module.exports = factory(require); }, this);
|
||||
@ -1,11 +0,0 @@
|
||||
if (typeof window !== "undefined") {
|
||||
window.define = function (factory) {
|
||||
try {
|
||||
delete window.define;
|
||||
} catch (e) {
|
||||
window.define = void 0; // IE
|
||||
}
|
||||
window.when = factory();
|
||||
};
|
||||
window.define.amd = {};
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"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": "~1.0.0",
|
||||
"faye-websocket": "~0.7.0",
|
||||
"when": "~2.4.0"
|
||||
"bane": "~1.1.0",
|
||||
"faye-websocket": "~0.7.2",
|
||||
"when": "~2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.6.13",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-buster": "~0.2.1",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-jshint": "~0.6.4",
|
||||
"grunt-contrib-uglify": "~0.2.4",
|
||||
"buster": "~0.7.8",
|
||||
"grunt": "~0.4.2",
|
||||
"grunt-buster": "~0.3.1",
|
||||
"grunt-browserify": "~1.3.0",
|
||||
"grunt-contrib-jshint": "~0.8.0",
|
||||
"grunt-contrib-uglify": "~0.2.7",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"phantomjs": "~1.9.2-0"
|
||||
"phantomjs": "~1.9.2-6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
/*global exports:false, require:false*/
|
||||
/*global module:true, require:false*/
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var bane = require("bane");
|
||||
var websocket = require("faye-websocket");
|
||||
var when = require("when");
|
||||
}
|
||||
var bane = require("bane");
|
||||
var websocket = require("../lib/websocket/");
|
||||
var when = require("when");
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
@ -26,11 +24,7 @@ function Mopidy(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
} else {
|
||||
Mopidy.WebSocket = window.WebSocket;
|
||||
}
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
var currentHost = (typeof document !== "undefined" &&
|
||||
@ -295,6 +289,4 @@ Mopidy.prototype._snakeToCamel = function (name) {
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof exports === "object") {
|
||||
exports.Mopidy = Mopidy;
|
||||
}
|
||||
module.exports = Mopidy;
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
/*global require:false, assert:false, refute:false*/
|
||||
/*global require:false */
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var buster = require("buster");
|
||||
var Mopidy = require("../src/mopidy").Mopidy;
|
||||
var Mopidy = require("../src/mopidy");
|
||||
var when = require("when");
|
||||
}
|
||||
|
||||
var assert = buster.assert;
|
||||
var refute = buster.refute;
|
||||
|
||||
buster.testCase("Mopidy", {
|
||||
setUp: function () {
|
||||
// Sinon.JS doesn't manage to stub PhantomJS' WebSocket implementation,
|
||||
|
||||
@ -40,11 +40,13 @@ def main():
|
||||
signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
|
||||
|
||||
try:
|
||||
registry = ext.Registry()
|
||||
|
||||
root_cmd = commands.RootCommand()
|
||||
config_cmd = commands.ConfigCommand()
|
||||
deps_cmd = commands.DepsCommand()
|
||||
|
||||
root_cmd.set(extension=None)
|
||||
root_cmd.set(extension=None, registry=registry)
|
||||
root_cmd.add_child('config', config_cmd)
|
||||
root_cmd.add_child('deps', deps_cmd)
|
||||
|
||||
@ -84,7 +86,6 @@ def main():
|
||||
enabled_extensions.append(extension)
|
||||
|
||||
log_extension_info(installed_extensions, enabled_extensions)
|
||||
ext.register_gstreamer_elements(enabled_extensions)
|
||||
|
||||
# Config and deps commands are simply special cased for now.
|
||||
if args.command == config_cmd:
|
||||
@ -108,10 +109,13 @@ def main():
|
||||
args.extension.ext_name)
|
||||
return 1
|
||||
|
||||
for extension in enabled_extensions:
|
||||
extension.setup(registry)
|
||||
|
||||
# Anything that wants to exit after this point must use
|
||||
# mopidy.utils.process.exit_process as actors can have been started.
|
||||
try:
|
||||
return args.command.run(args, proxied_config, enabled_extensions)
|
||||
return args.command.run(args, proxied_config)
|
||||
except NotImplementedError:
|
||||
print(root_cmd.format_help())
|
||||
return 1
|
||||
|
||||
@ -181,6 +181,12 @@ def audio_data_to_track(data):
|
||||
track_kwargs['uri'] = data['uri']
|
||||
track_kwargs['album'] = Album(**album_kwargs)
|
||||
|
||||
# TODO: this feels like a half assed workaround. we need to be sure that we
|
||||
# don't suddenly have lists in our models where we expect strings etc
|
||||
if ('genre' in track_kwargs and
|
||||
not isinstance(track_kwargs['genre'], basestring)):
|
||||
track_kwargs['genre'] = ', '.join(track_kwargs['genre'])
|
||||
|
||||
if ('name' in artist_kwargs
|
||||
and not isinstance(artist_kwargs['name'], basestring)):
|
||||
track_kwargs['artists'] = [Artist(name=artist)
|
||||
|
||||
@ -5,7 +5,6 @@ import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -22,25 +21,136 @@ class Extension(ext.Extension):
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['library'] = config.String()
|
||||
schema['media_dir'] = config.Path()
|
||||
schema['data_dir'] = config.Path()
|
||||
schema['playlists_dir'] = config.Path()
|
||||
schema['tag_cache_file'] = config.Deprecated()
|
||||
schema['scan_timeout'] = config.Integer(
|
||||
minimum=1000, maximum=1000*60*60)
|
||||
schema['scan_flush_threshold'] = config.Integer(minimum=0)
|
||||
schema['excluded_file_extensions'] = config.List(optional=True)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
try:
|
||||
path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy/local')
|
||||
except EnvironmentError as error:
|
||||
error = encoding.locale_decode(error)
|
||||
logger.warning('Could not create local data dir: %s', error)
|
||||
|
||||
def get_backend_classes(self):
|
||||
def setup(self, registry):
|
||||
from .actor import LocalBackend
|
||||
return [LocalBackend]
|
||||
from .json import JsonLibrary
|
||||
|
||||
LocalBackend.libraries = registry['local:library']
|
||||
|
||||
registry.add('backend', LocalBackend)
|
||||
registry.add('local:library', JsonLibrary)
|
||||
|
||||
def get_command(self):
|
||||
from .commands import LocalCommand
|
||||
return LocalCommand()
|
||||
|
||||
|
||||
class Library(object):
|
||||
"""
|
||||
Local library interface.
|
||||
|
||||
Extensions that wish to provide an alternate local library storage backend
|
||||
need to sub-class this class and install and configure it with an
|
||||
extension. Both scanning and library calls will use the active local
|
||||
library.
|
||||
|
||||
:param config: Config dictionary
|
||||
"""
|
||||
|
||||
#: Name of the local library implementation, must be overriden.
|
||||
name = None
|
||||
|
||||
def __init__(self, config):
|
||||
self._config = config
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
(Re)load any tracks stored in memory, if any, otherwise just return
|
||||
number of available tracks currently available. Will be called at
|
||||
startup for both library and update use cases, so if you plan to store
|
||||
tracks in memory this is when the should be (re)loaded.
|
||||
|
||||
:rtype: :class:`int` representing number of tracks in library.
|
||||
"""
|
||||
return 0
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup the given URI.
|
||||
|
||||
Unlike the core APIs, local tracks uris can only be resolved to a
|
||||
single track.
|
||||
|
||||
:param string uri: track URI
|
||||
:rtype: :class:`~mopidy.models.Track`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: remove uris, replacing it with support in query language.
|
||||
# TODO: remove exact, replacing it with support in query language.
|
||||
def search(self, query=None, limit=100, offset=0, exact=False, uris=None):
|
||||
"""
|
||||
Search the library for tracks where ``field`` contains ``values``.
|
||||
|
||||
:param dict query: one or more queries to search for
|
||||
:param int limit: maximum number of results to return
|
||||
:param int offset: offset into result set to use.
|
||||
:param bool exact: whether to look for exact matches
|
||||
:param uris: zero or more URI roots to limit the search to
|
||||
:type uris: list of strings or :class:`None`
|
||||
:rtype: :class:`~mopidy.models.SearchResult`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: add file browsing support.
|
||||
|
||||
# Remaining methods are use for the update process.
|
||||
def begin(self):
|
||||
"""
|
||||
Prepare library for accepting updates. Exactly what this means is
|
||||
highly implementation depended. This must however return an iterator
|
||||
that generates all tracks in the library for efficient scanning.
|
||||
|
||||
:rtype: :class:`~mopidy.models.Track` iterator
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def add(self, track):
|
||||
"""
|
||||
Add the given track to library.
|
||||
|
||||
:param :class:`~mopidy.models.Track` track: Track to add to the library
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, uri):
|
||||
"""
|
||||
Remove the given track from the library.
|
||||
|
||||
:param str uri: URI to remove from the library/
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def flush(self):
|
||||
"""
|
||||
Called for every n-th track indicating that work should be committed.
|
||||
Sub-classes are free to ignore these hints.
|
||||
|
||||
:rtype: Boolean indicating if state was flushed.
|
||||
"""
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close any resources used for updating, commit outstanding work etc.
|
||||
"""
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear out whatever data storage is used by this backend.
|
||||
|
||||
:rtype: Boolean indicating if state was cleared.
|
||||
"""
|
||||
return False
|
||||
|
||||
@ -8,13 +8,17 @@ import pykka
|
||||
from mopidy.backends import base
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
from .library import LocalLibraryProvider
|
||||
from .playback import LocalPlaybackProvider
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
uri_schemes = ['local']
|
||||
libraries = []
|
||||
|
||||
def __init__(self, config, audio):
|
||||
super(LocalBackend, self).__init__()
|
||||
|
||||
@ -22,16 +26,33 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
|
||||
self.check_dirs_and_files()
|
||||
|
||||
libraries = dict((l.name, l) for l in self.libraries)
|
||||
library_name = config['local']['library']
|
||||
|
||||
if library_name in libraries:
|
||||
library = libraries[library_name](config)
|
||||
logger.debug('Using %s as the local library', library_name)
|
||||
else:
|
||||
library = None
|
||||
logger.warning('Local library %s not found', library_name)
|
||||
|
||||
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = ['local']
|
||||
self.library = LocalLibraryProvider(backend=self, library=library)
|
||||
|
||||
def check_dirs_and_files(self):
|
||||
if not os.path.isdir(self.config['local']['media_dir']):
|
||||
logger.warning('Local media dir %s does not exist.' %
|
||||
self.config['local']['media_dir'])
|
||||
|
||||
try:
|
||||
path.get_or_create_dir(self.config['local']['data_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local data dir: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
# TODO: replace with data dir?
|
||||
try:
|
||||
path.get_or_create_dir(self.config['local']['playlists_dir'])
|
||||
except EnvironmentError as error:
|
||||
|
||||
@ -13,45 +13,72 @@ from . import translator
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_library(args, config):
|
||||
libraries = dict((l.name, l) for l in args.registry['local:library'])
|
||||
library_name = config['local']['library']
|
||||
|
||||
if library_name not in libraries:
|
||||
logger.warning('Local library %s not found', library_name)
|
||||
return 1
|
||||
|
||||
logger.debug('Using %s as the local library', library_name)
|
||||
return libraries[library_name](config)
|
||||
|
||||
|
||||
class LocalCommand(commands.Command):
|
||||
def __init__(self):
|
||||
super(LocalCommand, self).__init__()
|
||||
self.add_child('scan', ScanCommand())
|
||||
self.add_child('clear', ClearCommand())
|
||||
|
||||
|
||||
class ClearCommand(commands.Command):
|
||||
help = 'Clear local media files from the local library.'
|
||||
|
||||
def run(self, args, config):
|
||||
library = _get_library(args, config)
|
||||
prompt = 'Are you sure you want to clear the library? [y/N] '
|
||||
|
||||
if raw_input(prompt).lower() != 'y':
|
||||
logging.info('Clearing library aborted.')
|
||||
return 0
|
||||
|
||||
if library.clear():
|
||||
logging.info('Library succesfully cleared.')
|
||||
return 0
|
||||
|
||||
logging.warning('Unable to clear library.')
|
||||
return 1
|
||||
|
||||
|
||||
class ScanCommand(commands.Command):
|
||||
help = "Scan local media files and populate the local library."
|
||||
help = 'Scan local media files and populate the local library.'
|
||||
|
||||
def run(self, args, config, extensions):
|
||||
def __init__(self):
|
||||
super(ScanCommand, self).__init__()
|
||||
self.add_argument('--limit',
|
||||
action='store', type=int, dest='limit', default=None,
|
||||
help='Maxmimum number of tracks to scan')
|
||||
|
||||
def run(self, args, config):
|
||||
media_dir = config['local']['media_dir']
|
||||
scan_timeout = config['local']['scan_timeout']
|
||||
flush_threshold = config['local']['scan_flush_threshold']
|
||||
excluded_file_extensions = config['local']['excluded_file_extensions']
|
||||
excluded_file_extensions = set(
|
||||
ext.lower() for ext in config['local']['excluded_file_extensions'])
|
||||
file_ext.lower() for file_ext in excluded_file_extensions)
|
||||
|
||||
updaters = {}
|
||||
for e in extensions:
|
||||
for updater_class in e.get_library_updaters():
|
||||
if updater_class and 'local' in updater_class.uri_schemes:
|
||||
updaters[e.ext_name] = updater_class
|
||||
|
||||
if not updaters:
|
||||
logger.error('No usable library updaters found.')
|
||||
return 1
|
||||
elif len(updaters) > 1:
|
||||
logger.error('More than one library updater found. '
|
||||
'Provided by: %s', ', '.join(updaters.keys()))
|
||||
return 1
|
||||
|
||||
local_updater = updaters.values()[0](config)
|
||||
library = _get_library(args, config)
|
||||
|
||||
uri_path_mapping = {}
|
||||
uris_in_library = set()
|
||||
uris_to_update = set()
|
||||
uris_to_remove = set()
|
||||
|
||||
tracks = local_updater.load()
|
||||
logger.info('Checking %d tracks from library.', len(tracks))
|
||||
for track in tracks:
|
||||
num_tracks = library.load()
|
||||
logger.info('Checking %d tracks from library.', num_tracks)
|
||||
|
||||
for track in library.begin():
|
||||
uri_path_mapping[track.uri] = translator.local_track_uri_to_path(
|
||||
track.uri, media_dir)
|
||||
try:
|
||||
@ -65,16 +92,17 @@ class ScanCommand(commands.Command):
|
||||
|
||||
logger.info('Removing %d missing tracks.', len(uris_to_remove))
|
||||
for uri in uris_to_remove:
|
||||
local_updater.remove(uri)
|
||||
library.remove(uri)
|
||||
|
||||
logger.info('Checking %s for unknown tracks.', media_dir)
|
||||
for relpath in path.find_files(media_dir):
|
||||
uri = translator.path_to_local_track_uri(relpath)
|
||||
file_extension = os.path.splitext(relpath)[1]
|
||||
|
||||
if file_extension.lower() in excluded_file_extensions:
|
||||
logger.debug('Skipped %s: File extension excluded.', uri)
|
||||
continue
|
||||
|
||||
uri = translator.path_to_local_track_uri(relpath)
|
||||
if uri not in uris_in_library:
|
||||
uris_to_update.add(uri)
|
||||
uri_path_mapping[uri] = os.path.join(media_dir, relpath)
|
||||
@ -82,36 +110,44 @@ class ScanCommand(commands.Command):
|
||||
logger.info('Found %d unknown tracks.', len(uris_to_update))
|
||||
logger.info('Scanning...')
|
||||
|
||||
scanner = scan.Scanner(scan_timeout)
|
||||
progress = Progress(len(uris_to_update))
|
||||
uris_to_update = sorted(uris_to_update)[:args.limit]
|
||||
|
||||
for uri in sorted(uris_to_update):
|
||||
scanner = scan.Scanner(scan_timeout)
|
||||
progress = _Progress(flush_threshold, len(uris_to_update))
|
||||
|
||||
for uri in uris_to_update:
|
||||
try:
|
||||
data = scanner.scan(path.path_to_uri(uri_path_mapping[uri]))
|
||||
track = scan.audio_data_to_track(data).copy(uri=uri)
|
||||
local_updater.add(track)
|
||||
library.add(track)
|
||||
logger.debug('Added %s', track.uri)
|
||||
except exceptions.ScannerError as error:
|
||||
logger.warning('Failed %s: %s', uri, error)
|
||||
|
||||
progress.increment()
|
||||
if progress.increment():
|
||||
progress.log()
|
||||
if library.flush():
|
||||
logger.debug('Progress flushed.')
|
||||
|
||||
logger.info('Commiting changes.')
|
||||
local_updater.commit()
|
||||
progress.log()
|
||||
library.close()
|
||||
logger.info('Done scanning.')
|
||||
return 0
|
||||
|
||||
|
||||
# TODO: move to utils?
|
||||
class Progress(object):
|
||||
def __init__(self, total):
|
||||
class _Progress(object):
|
||||
def __init__(self, batch_size, total):
|
||||
self.count = 0
|
||||
self.batch_size = batch_size
|
||||
self.total = total
|
||||
self.start = time.time()
|
||||
|
||||
def increment(self):
|
||||
self.count += 1
|
||||
if self.count % 1000 == 0 or self.count == self.total:
|
||||
duration = time.time() - self.start
|
||||
remainder = duration / self.count * (self.total - self.count)
|
||||
logger.info('Scanned %d of %d files in %ds, ~%ds left.',
|
||||
self.count, self.total, duration, remainder)
|
||||
return self.count % self.batch_size == 0
|
||||
|
||||
def log(self):
|
||||
duration = time.time() - self.start
|
||||
remainder = duration / self.count * (self.total - self.count)
|
||||
logger.info('Scanned %d of %d files in %ds, ~%ds left.',
|
||||
self.count, self.total, duration, remainder)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
[local]
|
||||
enabled = true
|
||||
library = json
|
||||
media_dir = $XDG_MUSIC_DIR
|
||||
data_dir = $XDG_DATA_DIR/mopidy/local
|
||||
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
|
||||
scan_timeout = 1000
|
||||
scan_flush_threshold = 1000
|
||||
excluded_file_extensions =
|
||||
.html
|
||||
.jpeg
|
||||
|
||||
91
mopidy/backends/local/json.py
Normal file
91
mopidy/backends/local/json.py
Normal file
@ -0,0 +1,91 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import mopidy
|
||||
from mopidy import models
|
||||
from mopidy.backends import local
|
||||
from mopidy.backends.local import search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: move to load and dump in models?
|
||||
def load_library(json_file):
|
||||
try:
|
||||
with gzip.open(json_file, 'rb') as fp:
|
||||
return json.load(fp, object_hook=models.model_json_decoder)
|
||||
except (IOError, ValueError) as e:
|
||||
logger.warning('Loading JSON local library failed: %s', e)
|
||||
return {}
|
||||
|
||||
|
||||
def write_library(json_file, data):
|
||||
data['version'] = mopidy.__version__
|
||||
directory, basename = os.path.split(json_file)
|
||||
|
||||
# TODO: cleanup directory/basename.* files.
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix=basename + '.', dir=directory, delete=False)
|
||||
|
||||
try:
|
||||
with gzip.GzipFile(fileobj=tmp, mode='wb') as fp:
|
||||
json.dump(data, fp, cls=models.ModelJSONEncoder,
|
||||
indent=2, separators=(',', ': '))
|
||||
os.rename(tmp.name, json_file)
|
||||
finally:
|
||||
if os.path.exists(tmp.name):
|
||||
os.remove(tmp.name)
|
||||
|
||||
|
||||
class JsonLibrary(local.Library):
|
||||
name = b'json'
|
||||
|
||||
def __init__(self, config):
|
||||
self._tracks = {}
|
||||
self._media_dir = config['local']['media_dir']
|
||||
self._json_file = os.path.join(
|
||||
config['local']['data_dir'], b'library.json.gz')
|
||||
|
||||
def load(self):
|
||||
logger.debug('Loading json library from %s', self._json_file)
|
||||
library = load_library(self._json_file)
|
||||
self._tracks = dict((t.uri, t) for t in library.get('tracks', []))
|
||||
return len(self._tracks)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
return self._tracks[uri]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def search(self, query=None, limit=100, offset=0, uris=None, exact=False):
|
||||
tracks = self._tracks.values()
|
||||
# TODO: pass limit and offset into search helpers
|
||||
if exact:
|
||||
return search.find_exact(tracks, query=query, uris=uris)
|
||||
else:
|
||||
return search.search(tracks, query=query, uris=uris)
|
||||
|
||||
def begin(self):
|
||||
return self._tracks.itervalues()
|
||||
|
||||
def add(self, track):
|
||||
self._tracks[track.uri] = track
|
||||
|
||||
def remove(self, uri):
|
||||
self._tracks.pop(uri, None)
|
||||
|
||||
def close(self):
|
||||
write_library(self._json_file, {'tracks': self._tracks.values()})
|
||||
|
||||
def clear(self):
|
||||
try:
|
||||
os.remove(self._json_file)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
@ -1,30 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-Local-JSON'
|
||||
ext_name = 'local-json'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['json_file'] = config.Path()
|
||||
return schema
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .actor import LocalJsonBackend
|
||||
return [LocalJsonBackend]
|
||||
|
||||
def get_library_updaters(self):
|
||||
from .library import LocalJsonLibraryUpdateProvider
|
||||
return [LocalJsonLibraryUpdateProvider]
|
||||
@ -1,30 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.utils import encoding
|
||||
|
||||
from . import library
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalJsonBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, config, audio):
|
||||
super(LocalJsonBackend, self).__init__()
|
||||
|
||||
self.config = config
|
||||
self.library = library.LocalJsonLibraryProvider(backend=self)
|
||||
self.uri_schemes = ['local']
|
||||
|
||||
if not os.path.exists(config['local-json']['json_file']):
|
||||
try:
|
||||
library.write_library(config['local-json']['json_file'], {})
|
||||
logger.info('Created empty local JSON library.')
|
||||
except EnvironmentError as error:
|
||||
error = encoding.locale_decode(error)
|
||||
logger.warning('Could not create local library: %s', error)
|
||||
@ -1,3 +0,0 @@
|
||||
[local-json]
|
||||
enabled = true
|
||||
json_file = $XDG_DATA_DIR/mopidy/local/library.json.gz
|
||||
@ -1,110 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import mopidy
|
||||
from mopidy import models
|
||||
from mopidy.backends import base
|
||||
from mopidy.backends.local import search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_library(json_file):
|
||||
try:
|
||||
with gzip.open(json_file, 'rb') as fp:
|
||||
return json.load(fp, object_hook=models.model_json_decoder)
|
||||
except (IOError, ValueError) as e:
|
||||
logger.warning('Loading JSON local library failed: %s', e)
|
||||
return {}
|
||||
|
||||
|
||||
def write_library(json_file, data):
|
||||
data['version'] = mopidy.__version__
|
||||
directory, basename = os.path.split(json_file)
|
||||
|
||||
# TODO: cleanup directory/basename.* files.
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix=basename + '.', dir=directory, delete=False)
|
||||
|
||||
try:
|
||||
with gzip.GzipFile(fileobj=tmp, mode='wb') as fp:
|
||||
json.dump(data, fp, cls=models.ModelJSONEncoder,
|
||||
indent=2, separators=(',', ': '))
|
||||
os.rename(tmp.name, json_file)
|
||||
finally:
|
||||
if os.path.exists(tmp.name):
|
||||
os.remove(tmp.name)
|
||||
|
||||
|
||||
class LocalJsonLibraryProvider(base.BaseLibraryProvider):
|
||||
root_directory_name = 'local'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalJsonLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._uri_mapping = {}
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._json_file = self.backend.config['local-json']['json_file']
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, uri=None):
|
||||
logger.debug(
|
||||
'Loading local tracks from %s using %s',
|
||||
self._media_dir, self._json_file)
|
||||
|
||||
tracks = load_library(self._json_file).get('tracks', [])
|
||||
uris_to_remove = set(self._uri_mapping)
|
||||
|
||||
for track in tracks:
|
||||
self._uri_mapping[track.uri] = track
|
||||
uris_to_remove.discard(track.uri)
|
||||
|
||||
for uri in uris_to_remove:
|
||||
del self._uri_mapping[uri]
|
||||
|
||||
logger.info(
|
||||
'Loaded %d local tracks from %s using %s',
|
||||
len(tracks), self._media_dir, self._json_file)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
return [self._uri_mapping[uri]]
|
||||
except KeyError:
|
||||
logger.debug('Failed to lookup %r', uri)
|
||||
return []
|
||||
|
||||
def find_exact(self, query=None, uris=None):
|
||||
tracks = self._uri_mapping.values()
|
||||
return search.find_exact(tracks, query=query, uris=uris)
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
tracks = self._uri_mapping.values()
|
||||
return search.search(tracks, query=query, uris=uris)
|
||||
|
||||
|
||||
class LocalJsonLibraryUpdateProvider(base.BaseLibraryProvider):
|
||||
uri_schemes = ['local']
|
||||
|
||||
def __init__(self, config):
|
||||
self._tracks = {}
|
||||
self._media_dir = config['local']['media_dir']
|
||||
self._json_file = config['local-json']['json_file']
|
||||
|
||||
def load(self):
|
||||
for track in load_library(self._json_file).get('tracks', []):
|
||||
self._tracks[track.uri] = track
|
||||
return self._tracks.values()
|
||||
|
||||
def add(self, track):
|
||||
self._tracks[track.uri] = track
|
||||
|
||||
def remove(self, uri):
|
||||
if uri in self._tracks:
|
||||
del self._tracks[uri]
|
||||
|
||||
def commit(self):
|
||||
write_library(self._json_file, {'tracks': self._tracks.values()})
|
||||
44
mopidy/backends/local/library.py
Normal file
44
mopidy/backends/local/library.py
Normal file
@ -0,0 +1,44 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy.backends import base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
"""Proxy library that delegates work to our active local library."""
|
||||
|
||||
root_directory_name = 'local'
|
||||
|
||||
def __init__(self, backend, library):
|
||||
super(LocalLibraryProvider, self).__init__(backend)
|
||||
self._library = library
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, uri=None):
|
||||
if not self._library:
|
||||
return 0
|
||||
num_tracks = self._library.load()
|
||||
logger.info('Loaded %d local tracks using %s',
|
||||
num_tracks, self._library.name)
|
||||
|
||||
def lookup(self, uri):
|
||||
if not self._library:
|
||||
return []
|
||||
track = self._library.lookup(uri)
|
||||
if track is None:
|
||||
logger.debug('Failed to lookup %r', uri)
|
||||
return []
|
||||
return [track]
|
||||
|
||||
def find_exact(self, query=None, uris=None):
|
||||
if not self._library:
|
||||
return None
|
||||
return self._library.search(query=query, uris=uris, exact=True)
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
if not self._library:
|
||||
return None
|
||||
return self._library.search(query=query, uris=uris, exact=False)
|
||||
@ -257,22 +257,26 @@ class RootCommand(Command):
|
||||
type=config_override_type, metavar='OPTIONS',
|
||||
help='`section/key=value` values to override config options')
|
||||
|
||||
def run(self, args, config, extensions):
|
||||
def run(self, args, config):
|
||||
loop = gobject.MainLoop()
|
||||
|
||||
backend_classes = args.registry['backend']
|
||||
frontend_classes = args.registry['frontend']
|
||||
|
||||
try:
|
||||
audio = self.start_audio(config)
|
||||
backends = self.start_backends(config, extensions, audio)
|
||||
backends = self.start_backends(config, backend_classes, audio)
|
||||
core = self.start_core(audio, backends)
|
||||
self.start_frontends(config, extensions, core)
|
||||
self.start_frontends(config, frontend_classes, core)
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Interrupted. Exiting...')
|
||||
return
|
||||
finally:
|
||||
loop.quit()
|
||||
self.stop_frontends(extensions)
|
||||
self.stop_frontends(frontend_classes)
|
||||
self.stop_core()
|
||||
self.stop_backends(extensions)
|
||||
self.stop_backends(backend_classes)
|
||||
self.stop_audio()
|
||||
process.stop_remaining_actors()
|
||||
|
||||
@ -280,11 +284,7 @@ class RootCommand(Command):
|
||||
logger.info('Starting Mopidy audio')
|
||||
return Audio.start(config=config).proxy()
|
||||
|
||||
def start_backends(self, config, extensions, audio):
|
||||
backend_classes = []
|
||||
for extension in extensions:
|
||||
backend_classes.extend(extension.get_backend_classes())
|
||||
|
||||
def start_backends(self, config, backend_classes, audio):
|
||||
logger.info(
|
||||
'Starting Mopidy backends: %s',
|
||||
', '.join(b.__name__ for b in backend_classes) or 'none')
|
||||
@ -300,11 +300,7 @@ class RootCommand(Command):
|
||||
logger.info('Starting Mopidy core')
|
||||
return Core.start(audio=audio, backends=backends).proxy()
|
||||
|
||||
def start_frontends(self, config, extensions, core):
|
||||
frontend_classes = []
|
||||
for extension in extensions:
|
||||
frontend_classes.extend(extension.get_frontend_classes())
|
||||
|
||||
def start_frontends(self, config, frontend_classes, core):
|
||||
logger.info(
|
||||
'Starting Mopidy frontends: %s',
|
||||
', '.join(f.__name__ for f in frontend_classes) or 'none')
|
||||
@ -312,21 +308,19 @@ class RootCommand(Command):
|
||||
for frontend_class in frontend_classes:
|
||||
frontend_class.start(config=config, core=core)
|
||||
|
||||
def stop_frontends(self, extensions):
|
||||
def stop_frontends(self, frontend_classes):
|
||||
logger.info('Stopping Mopidy frontends')
|
||||
for extension in extensions:
|
||||
for frontend_class in extension.get_frontend_classes():
|
||||
process.stop_actors_by_class(frontend_class)
|
||||
for frontend_class in frontend_classes:
|
||||
process.stop_actors_by_class(frontend_class)
|
||||
|
||||
def stop_core(self):
|
||||
logger.info('Stopping Mopidy core')
|
||||
process.stop_actors_by_class(Core)
|
||||
|
||||
def stop_backends(self, extensions):
|
||||
def stop_backends(self, backend_classes):
|
||||
logger.info('Stopping Mopidy backends')
|
||||
for extension in extensions:
|
||||
for backend_class in extension.get_backend_classes():
|
||||
process.stop_actors_by_class(backend_class)
|
||||
for backend_class in backend_classes:
|
||||
process.stop_actors_by_class(backend_class)
|
||||
|
||||
def stop_audio(self):
|
||||
logger.info('Stopping Mopidy audio')
|
||||
|
||||
@ -92,32 +92,29 @@ class Backends(list):
|
||||
self.with_playback = collections.OrderedDict()
|
||||
self.with_playlists = collections.OrderedDict()
|
||||
|
||||
backends_by_scheme = {}
|
||||
name = lambda backend: backend.actor_ref.actor_class.__name__
|
||||
|
||||
for backend in backends:
|
||||
has_library = backend.has_library().get()
|
||||
has_playback = backend.has_playback().get()
|
||||
has_playlists = backend.has_playlists().get()
|
||||
|
||||
for scheme in backend.uri_schemes.get():
|
||||
self.add(self.with_library, has_library, scheme, backend)
|
||||
self.add(self.with_playback, has_playback, scheme, backend)
|
||||
self.add(self.with_playlists, has_playlists, scheme, backend)
|
||||
assert scheme not in backends_by_scheme, (
|
||||
'Cannot add URI scheme %s for %s, '
|
||||
'it is already handled by %s'
|
||||
) % (scheme, name(backend), name(backends_by_scheme[scheme]))
|
||||
backends_by_scheme[scheme] = backend
|
||||
|
||||
if has_library:
|
||||
self.with_library[scheme] = backend
|
||||
if has_playback:
|
||||
self.with_playback[scheme] = backend
|
||||
if has_playlists:
|
||||
self.with_playlists[scheme] = backend
|
||||
|
||||
if has_library:
|
||||
root_dir_name = backend.library.root_directory_name.get()
|
||||
has_browsable_library = root_dir_name is not None
|
||||
self.add(
|
||||
self.with_browsable_library, has_browsable_library,
|
||||
root_dir_name, backend)
|
||||
|
||||
def add(self, registry, supported, uri_scheme, backend):
|
||||
if not supported:
|
||||
return
|
||||
|
||||
if uri_scheme not in registry:
|
||||
registry[uri_scheme] = backend
|
||||
return
|
||||
|
||||
get_name = lambda actor: actor.actor_ref.actor_class.__name__
|
||||
raise AssertionError(
|
||||
'Cannot add URI scheme %s for %s, it is already handled by %s' %
|
||||
(uri_scheme, get_name(backend), get_name(registry[uri_scheme])))
|
||||
if root_dir_name is not None:
|
||||
self.with_browsable_library[root_dir_name] = backend
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import pkg_resources
|
||||
|
||||
@ -61,6 +62,15 @@ class Extension(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup(self, registry):
|
||||
for backend_class in self.get_backend_classes():
|
||||
registry.add('backend', backend_class)
|
||||
|
||||
for frontend_class in self.get_frontend_classes():
|
||||
registry.add('frontend', frontend_class)
|
||||
|
||||
self.register_gstreamer_elements()
|
||||
|
||||
def get_frontend_classes(self):
|
||||
"""List of frontend actor classes
|
||||
|
||||
@ -79,6 +89,7 @@ class Extension(object):
|
||||
"""
|
||||
return []
|
||||
|
||||
# TODO: remove
|
||||
def get_library_updaters(self):
|
||||
"""List of library updater classes
|
||||
|
||||
@ -112,6 +123,24 @@ class Extension(object):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: document
|
||||
class Registry(collections.Mapping):
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
|
||||
def add(self, name, cls):
|
||||
self._registry.setdefault(name, []).append(cls)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._registry.setdefault(name, [])
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._registry)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._registry)
|
||||
|
||||
|
||||
def load_extensions():
|
||||
"""Find all installed extensions.
|
||||
|
||||
@ -166,15 +195,3 @@ def validate_extension(extension):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register_gstreamer_elements(enabled_extensions):
|
||||
"""Registers custom GStreamer elements from extensions.
|
||||
|
||||
:param enabled_extensions: list of enabled extensions
|
||||
"""
|
||||
|
||||
for extension in enabled_extensions:
|
||||
logger.debug(
|
||||
'Registering GStreamer elements for: %s', extension.ext_name)
|
||||
extension.register_gstreamer_elements()
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
/*! Mopidy.js - built 2013-09-17
|
||||
/*! Mopidy.js - built 2014-01-04
|
||||
* http://www.mopidy.com/
|
||||
* Copyright (c) 2013 Stein Magnus Jodal and contributors
|
||||
* Copyright (c) 2014 Stein Magnus Jodal and contributors
|
||||
* Licensed under the Apache License, Version 2.0 */
|
||||
!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Mopidy=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
module.exports = { Client: window.WebSocket };
|
||||
|
||||
},{}],2:[function(require,module,exports){
|
||||
((typeof define === "function" && define.amd && function (m) { define("bane", m); }) ||
|
||||
(typeof module === "object" && function (m) { module.exports = m(); }) ||
|
||||
function (m) { this.bane = m(); }
|
||||
@ -46,7 +50,7 @@
|
||||
|
||||
/**
|
||||
* @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).
|
||||
*/
|
||||
@ -163,21 +167,78 @@
|
||||
return object;
|
||||
}
|
||||
|
||||
return { createEventEmitter: createEventEmitter };
|
||||
return {
|
||||
createEventEmitter: createEventEmitter,
|
||||
aggregate: function (emitters) {
|
||||
var aggregate = createEventEmitter();
|
||||
emitters.forEach(function (emitter) {
|
||||
emitter.on(function (event, data) {
|
||||
aggregate.emit(event, data);
|
||||
});
|
||||
});
|
||||
return aggregate;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.define = function (factory) {
|
||||
try {
|
||||
delete window.define;
|
||||
} catch (e) {
|
||||
window.define = void 0; // IE
|
||||
}
|
||||
window.when = factory();
|
||||
},{}],3:[function(require,module,exports){
|
||||
// shim for using process in browser
|
||||
|
||||
var process = module.exports = {};
|
||||
|
||||
process.nextTick = (function () {
|
||||
var canSetImmediate = typeof window !== 'undefined'
|
||||
&& window.setImmediate;
|
||||
var canPost = typeof window !== 'undefined'
|
||||
&& window.postMessage && window.addEventListener
|
||||
;
|
||||
|
||||
if (canSetImmediate) {
|
||||
return function (f) { return window.setImmediate(f) };
|
||||
}
|
||||
|
||||
if (canPost) {
|
||||
var queue = [];
|
||||
window.addEventListener('message', function (ev) {
|
||||
var source = ev.source;
|
||||
if ((source === window || source === null) && ev.data === 'process-tick') {
|
||||
ev.stopPropagation();
|
||||
if (queue.length > 0) {
|
||||
var fn = queue.shift();
|
||||
fn();
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
return function nextTick(fn) {
|
||||
queue.push(fn);
|
||||
window.postMessage('process-tick', '*');
|
||||
};
|
||||
}
|
||||
|
||||
return function nextTick(fn) {
|
||||
setTimeout(fn, 0);
|
||||
};
|
||||
window.define.amd = {};
|
||||
})();
|
||||
|
||||
process.title = 'browser';
|
||||
process.browser = true;
|
||||
process.env = {};
|
||||
process.argv = [];
|
||||
|
||||
process.binding = function (name) {
|
||||
throw new Error('process.binding is not supported');
|
||||
}
|
||||
|
||||
// TODO(shtylman)
|
||||
process.cwd = function () { return '/' };
|
||||
process.chdir = function (dir) {
|
||||
throw new Error('process.chdir is not supported');
|
||||
};
|
||||
|
||||
},{}],4:[function(require,module,exports){
|
||||
var process=require("__browserify_process");/** @license MIT License (c) copyright 2011-2013 original author or authors */
|
||||
|
||||
/**
|
||||
* A lightweight CommonJS Promises/A and when() implementation
|
||||
* when is part of the cujo.js family of libraries (http://cujojs.com/)
|
||||
@ -187,9 +248,9 @@ if (typeof window !== "undefined") {
|
||||
*
|
||||
* @author Brian Cavalier
|
||||
* @author John Hann
|
||||
* @version 2.4.0
|
||||
* @version 2.7.1
|
||||
*/
|
||||
(function(define, global) { 'use strict';
|
||||
(function(define) { 'use strict';
|
||||
define(function (require) {
|
||||
|
||||
// Public API
|
||||
@ -230,7 +291,17 @@ define(function (require) {
|
||||
function when(promiseOrValue, onFulfilled, onRejected, onProgress) {
|
||||
// Get a trusted promise for the input promiseOrValue, and then
|
||||
// register promise handlers
|
||||
return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress);
|
||||
return cast(promiseOrValue).then(onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise whose fate is determined by resolver.
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
*/
|
||||
function promise(resolver) {
|
||||
return new Promise(resolver,
|
||||
monitorApi.PromiseStatus && monitorApi.PromiseStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,117 +309,214 @@ define(function (require) {
|
||||
* 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
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
* @name Promise
|
||||
*/
|
||||
function Promise(sendMessage, inspect) {
|
||||
this._message = sendMessage;
|
||||
function Promise(resolver, status) {
|
||||
var self, value, consumers = [];
|
||||
|
||||
self = this;
|
||||
this._status = status;
|
||||
this.inspect = inspect;
|
||||
this._when = _when;
|
||||
|
||||
// Call the provider resolver to seal the promise's fate
|
||||
try {
|
||||
resolver(promiseResolve, promiseReject, promiseNotify);
|
||||
} catch(e) {
|
||||
promiseReject(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of this promise's current status at the instant of call
|
||||
* @returns {{state:String}}
|
||||
*/
|
||||
function inspect() {
|
||||
return value ? value.inspect() : toPendingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private message delivery. Queues and delivers messages to
|
||||
* the promise's ultimate fulfillment value or rejection reason.
|
||||
* @private
|
||||
*/
|
||||
function _when(resolve, notify, onFulfilled, onRejected, onProgress) {
|
||||
consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); });
|
||||
|
||||
function deliver(p) {
|
||||
p._when(resolve, notify, onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the ultimate fulfillment or rejection
|
||||
* @param {*} val resolution value
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
if(!consumers) {
|
||||
return;
|
||||
}
|
||||
|
||||
var queue = consumers;
|
||||
consumers = undef;
|
||||
|
||||
enqueue(function () {
|
||||
value = coerce(self, val);
|
||||
if(status) {
|
||||
updateStatus(value, status);
|
||||
}
|
||||
runHandlers(queue, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject this promise with the supplied reason, which will be used verbatim.
|
||||
* @param {*} reason reason for the rejection
|
||||
*/
|
||||
function promiseReject(reason) {
|
||||
promiseResolve(new RejectedPromise(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @param {*} update progress event payload to pass to all listeners
|
||||
*/
|
||||
function promiseNotify(update) {
|
||||
if(consumers) {
|
||||
var queue = consumers;
|
||||
enqueue(function () {
|
||||
runHandlers(queue, new ProgressingPromise(update));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
promisePrototype = Promise.prototype;
|
||||
|
||||
args = arguments;
|
||||
sendMessage = this._message;
|
||||
/**
|
||||
* Register handlers for this promise.
|
||||
* @param [onFulfilled] {Function} fulfillment handler
|
||||
* @param [onRejected] {Function} rejection handler
|
||||
* @param [onProgress] {Function} progress handler
|
||||
* @return {Promise} new Promise
|
||||
*/
|
||||
promisePrototype.then = function(onFulfilled, onRejected, onProgress) {
|
||||
var self = this;
|
||||
|
||||
return _promise(function(resolve, reject, notify) {
|
||||
sendMessage('when', args, resolve, notify);
|
||||
}, this._status && this._status.observed());
|
||||
},
|
||||
return new Promise(function(resolve, reject, notify) {
|
||||
self._when(resolve, notify, onFulfilled, onRejected, onProgress);
|
||||
}, this._status && this._status.observed());
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
|
||||
* @param {function?} onRejected
|
||||
* @return {Promise}
|
||||
*/
|
||||
otherwise: function(onRejected) {
|
||||
return this.then(undef, onRejected);
|
||||
},
|
||||
/**
|
||||
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
|
||||
* @param {function?} onRejected
|
||||
* @return {Promise}
|
||||
*/
|
||||
promisePrototype['catch'] = promisePrototype.otherwise = function(onRejected) {
|
||||
return this.then(undef, onRejected);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures that onFulfilledOrRejected will be called regardless of whether
|
||||
* this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT
|
||||
* receive the promises' value or reason. Any returned value will be disregarded.
|
||||
* onFulfilledOrRejected may throw or return a rejected promise to signal
|
||||
* an additional error.
|
||||
* @param {function} onFulfilledOrRejected handler to be called regardless of
|
||||
* fulfillment or rejection
|
||||
* @returns {Promise}
|
||||
*/
|
||||
ensure: function(onFulfilledOrRejected) {
|
||||
return this.then(injectHandler, injectHandler)['yield'](this);
|
||||
/**
|
||||
* Ensures that onFulfilledOrRejected will be called regardless of whether
|
||||
* this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT
|
||||
* receive the promises' value or reason. Any returned value will be disregarded.
|
||||
* onFulfilledOrRejected may throw or return a rejected promise to signal
|
||||
* an additional error.
|
||||
* @param {function} onFulfilledOrRejected handler to be called regardless of
|
||||
* fulfillment or rejection
|
||||
* @returns {Promise}
|
||||
*/
|
||||
promisePrototype['finally'] = promisePrototype.ensure = function(onFulfilledOrRejected) {
|
||||
return typeof onFulfilledOrRejected === 'function'
|
||||
? this.then(injectHandler, injectHandler)['yield'](this)
|
||||
: this;
|
||||
|
||||
function injectHandler() {
|
||||
return resolve(onFulfilledOrRejected());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(function() { return value; })
|
||||
* @param {*} value
|
||||
* @return {Promise} a promise that:
|
||||
* - is fulfilled if value is not a promise, or
|
||||
* - if value is a promise, will fulfill with its value, or reject
|
||||
* with its reason.
|
||||
*/
|
||||
'yield': function(value) {
|
||||
return this.then(function() {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* i.e. onFulfilled.apply(undefined, array).
|
||||
* @param {function} onFulfilled function to receive spread arguments
|
||||
* @return {Promise}
|
||||
*/
|
||||
spread: function(onFulfilled) {
|
||||
return this.then(function(array) {
|
||||
// array may contain promises, so resolve its contents.
|
||||
return all(array, function(array) {
|
||||
return onFulfilled.apply(undef, array);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected)
|
||||
* @deprecated
|
||||
*/
|
||||
always: function(onFulfilledOrRejected, onProgress) {
|
||||
return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress);
|
||||
function injectHandler() {
|
||||
return resolve(onFulfilledOrRejected());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Terminate a promise chain by handling the ultimate fulfillment value or
|
||||
* rejection reason, and assuming responsibility for all errors. if an
|
||||
* error propagates out of handleResult or handleFatalError, it will be
|
||||
* rethrown to the host, resulting in a loud stack track on most platforms
|
||||
* and a crash on some.
|
||||
* @param {function?} handleResult
|
||||
* @param {function?} handleError
|
||||
* @returns {undefined}
|
||||
*/
|
||||
promisePrototype.done = function(handleResult, handleError) {
|
||||
this.then(handleResult, handleError)['catch'](crash);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for .then(function() { return value; })
|
||||
* @param {*} value
|
||||
* @return {Promise} a promise that:
|
||||
* - is fulfilled if value is not a promise, or
|
||||
* - if value is a promise, will fulfill with its value, or reject
|
||||
* with its reason.
|
||||
*/
|
||||
promisePrototype['yield'] = function(value) {
|
||||
return this.then(function() {
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a side effect when this promise fulfills, without changing the
|
||||
* fulfillment value.
|
||||
* @param {function} onFulfilledSideEffect
|
||||
* @returns {Promise}
|
||||
*/
|
||||
promisePrototype.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
|
||||
* i.e. onFulfilled.apply(undefined, array).
|
||||
* @param {function} onFulfilled function to receive spread arguments
|
||||
* @return {Promise}
|
||||
*/
|
||||
promisePrototype.spread = function(onFulfilled) {
|
||||
return this.then(function(array) {
|
||||
// array may contain promises, so resolve its contents.
|
||||
return all(array, function(array) {
|
||||
return onFulfilled.apply(undef, array);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected)
|
||||
* @deprecated
|
||||
*/
|
||||
promisePrototype.always = function(onFulfilledOrRejected, onProgress) {
|
||||
return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress);
|
||||
};
|
||||
|
||||
/**
|
||||
* Casts x to a trusted promise. If x is already a trusted promise, it is
|
||||
* returned, otherwise a new trusted Promise which follows x is returned.
|
||||
* @param {*} x
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function cast(x) {
|
||||
return x instanceof Promise ? x : resolve(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a resolved promise. The returned promise will be
|
||||
* - fulfilled with promiseOrValue if it is a value, or
|
||||
* - if promiseOrValue is a promise
|
||||
* - fulfilled with promiseOrValue's value after it is fulfilled
|
||||
* - rejected with promiseOrValue's reason after it is rejected
|
||||
* In contract to cast(x), this always creates a new Promise
|
||||
* @param {*} value
|
||||
* @return {Promise}
|
||||
*/
|
||||
@ -369,7 +537,9 @@ define(function (require) {
|
||||
* @return {Promise} rejected {@link Promise}
|
||||
*/
|
||||
function reject(promiseOrValue) {
|
||||
return when(promiseOrValue, rejected);
|
||||
return when(promiseOrValue, function(e) {
|
||||
return new RejectedPromise(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -414,7 +584,7 @@ define(function (require) {
|
||||
|
||||
deferred.reject = deferred.resolver.reject = function(reason) {
|
||||
if(resolved) {
|
||||
return resolve(rejected(reason));
|
||||
return resolve(new RejectedPromise(reason));
|
||||
}
|
||||
resolved = true;
|
||||
rejectPending(reason);
|
||||
@ -429,169 +599,17 @@ define(function (require) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise whose fate is determined by resolver.
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
* Run a queue of functions as quickly as possible, passing
|
||||
* value to each.
|
||||
*/
|
||||
function promise(resolver) {
|
||||
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 {
|
||||
resolver(promiseResolve, promiseReject, promiseNotify);
|
||||
} catch(e) {
|
||||
promiseReject(e);
|
||||
function runHandlers(queue, value) {
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
queue[i](value);
|
||||
}
|
||||
|
||||
// Return the promise
|
||||
return self;
|
||||
|
||||
/**
|
||||
* 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 _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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the ultimate fulfillment or rejection
|
||||
* @param {*|Promise} val resolution value
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
if(!consumers) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = coerce(val);
|
||||
scheduleConsumers(consumers, value);
|
||||
consumers = undef;
|
||||
|
||||
if(status) {
|
||||
updateStatus(value, status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject this promise with the supplied reason, which will be used verbatim.
|
||||
* @param {*} reason reason for the rejection
|
||||
*/
|
||||
function promiseReject(reason) {
|
||||
promiseResolve(rejected(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @param {*} update progress event payload to pass to all listeners
|
||||
*/
|
||||
function promiseNotify(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 {*} Guaranteed to return a trusted Promise. If x
|
||||
* is trusted, returns x, otherwise, returns a new, trusted, already-resolved
|
||||
@ -599,83 +617,121 @@ define(function (require) {
|
||||
* * the resolution value of x if it's a foreign promise, or
|
||||
* * x if it's a value
|
||||
*/
|
||||
function coerce(x) {
|
||||
function coerce(self, x) {
|
||||
if (x === self) {
|
||||
return new RejectedPromise(new TypeError());
|
||||
}
|
||||
|
||||
if (x instanceof Promise) {
|
||||
return x;
|
||||
}
|
||||
|
||||
if (!(x === Object(x) && 'then' in x)) {
|
||||
return fulfilled(x);
|
||||
try {
|
||||
var untrustedThen = x === Object(x) && x.then;
|
||||
|
||||
return typeof untrustedThen === 'function'
|
||||
? assimilate(untrustedThen, x)
|
||||
: new FulfilledPromise(x);
|
||||
} catch(e) {
|
||||
return new RejectedPromise(e);
|
||||
}
|
||||
|
||||
return promise(function(resolve, reject, notify) {
|
||||
enqueue(function() {
|
||||
try {
|
||||
// We must check and assimilate in the same tick, but not the
|
||||
// current tick, careful only to access promiseOrValue.then once.
|
||||
var untrustedThen = x.then;
|
||||
|
||||
if(typeof untrustedThen === 'function') {
|
||||
fcall(untrustedThen, x, resolve, reject, notify);
|
||||
} else {
|
||||
// It's a value, create a fulfilled wrapper
|
||||
resolve(fulfilled(x));
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// Something went wrong, reject
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for a near, fulfilled value
|
||||
* @param {*} value
|
||||
* @constructor
|
||||
* Safely assimilates a foreign thenable by wrapping it in a trusted promise
|
||||
* @param {function} untrustedThen x's then() method
|
||||
* @param {object|function} x thenable
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function NearFulfilledProxy(value) {
|
||||
function assimilate(untrustedThen, x) {
|
||||
return promise(function (resolve, reject) {
|
||||
fcall(untrustedThen, x, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
makePromisePrototype = Object.create ||
|
||||
function(o) {
|
||||
function PromisePrototype() {}
|
||||
PromisePrototype.prototype = o;
|
||||
return new PromisePrototype();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a fulfilled, local promise as a proxy for a value
|
||||
* NOTE: must never be exposed
|
||||
* @private
|
||||
* @param {*} value fulfillment value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function FulfilledPromise(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
NearFulfilledProxy.prototype.when = function(onResult) {
|
||||
return typeof onResult === 'function' ? onResult(this.value) : this.value;
|
||||
FulfilledPromise.prototype = makePromisePrototype(promisePrototype);
|
||||
|
||||
FulfilledPromise.prototype.inspect = function() {
|
||||
return toFulfilledState(this.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy for a near rejection
|
||||
* @param {*} reason
|
||||
* @constructor
|
||||
*/
|
||||
function NearRejectedProxy(reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
NearRejectedProxy.prototype.when = function(_, onError) {
|
||||
if(typeof onError === 'function') {
|
||||
return onError(this.reason);
|
||||
} else {
|
||||
throw this.reason;
|
||||
FulfilledPromise.prototype._when = function(resolve, _, onFulfilled) {
|
||||
try {
|
||||
resolve(typeof onFulfilled === 'function' ? onFulfilled(this.value) : this.value);
|
||||
} catch(e) {
|
||||
resolve(new RejectedPromise(e));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule a task that will process a list of handlers
|
||||
* in the next queue drain run.
|
||||
* Creates a rejected, local promise as a proxy for a value
|
||||
* NOTE: must never be exposed
|
||||
* @private
|
||||
* @param {Array} handlers queue of handlers to execute
|
||||
* @param {*} value passed as the only arg to each handler
|
||||
* @param {*} reason rejection reason
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function scheduleConsumers(handlers, value) {
|
||||
enqueue(function() {
|
||||
var handler, i = 0;
|
||||
while (handler = handlers[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
});
|
||||
function RejectedPromise(reason) {
|
||||
this.value = reason;
|
||||
}
|
||||
|
||||
RejectedPromise.prototype = makePromisePrototype(promisePrototype);
|
||||
|
||||
RejectedPromise.prototype.inspect = function() {
|
||||
return toRejectedState(this.value);
|
||||
};
|
||||
|
||||
RejectedPromise.prototype._when = function(resolve, _, __, onRejected) {
|
||||
try {
|
||||
resolve(typeof onRejected === 'function' ? onRejected(this.value) : this);
|
||||
} catch(e) {
|
||||
resolve(new RejectedPromise(e));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a progress promise with the supplied update.
|
||||
* @private
|
||||
* @param {*} value progress update value
|
||||
* @return {Promise} progress promise
|
||||
*/
|
||||
function ProgressingPromise(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
ProgressingPromise.prototype = makePromisePrototype(promisePrototype);
|
||||
|
||||
ProgressingPromise.prototype._when = function(_, notify, f, r, u) {
|
||||
try {
|
||||
notify(typeof u === 'function' ? u(this.value) : this.value);
|
||||
} catch(e) {
|
||||
notify(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a PromiseStatus monitor object with the outcome
|
||||
* of the supplied value promise.
|
||||
* @param {Promise} value
|
||||
* @param {PromiseStatus} status
|
||||
*/
|
||||
function updateStatus(value, status) {
|
||||
value.then(statusFulfilled, statusRejected);
|
||||
|
||||
@ -852,7 +908,7 @@ define(function (require) {
|
||||
function _map(array, mapFunc, fallback) {
|
||||
return when(array, function(array) {
|
||||
|
||||
return _promise(resolveMap);
|
||||
return new Promise(resolveMap);
|
||||
|
||||
function resolveMap(resolve, reject, notify) {
|
||||
var results, len, toResolve, i;
|
||||
@ -879,12 +935,11 @@ define(function (require) {
|
||||
function resolveOne(item, i) {
|
||||
when(item, mapFunc, fallback).then(function(mapped) {
|
||||
results[i] = mapped;
|
||||
notify(mapped);
|
||||
|
||||
if(!--toResolve) {
|
||||
resolve(results);
|
||||
}
|
||||
}, reject);
|
||||
}, reject, notify);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -960,9 +1015,9 @@ define(function (require) {
|
||||
// Internals, utilities, etc.
|
||||
//
|
||||
|
||||
var reduceArray, slice, fcall, nextTick, handlerQueue,
|
||||
setTimeout, funcProto, call, arrayProto, monitorApi,
|
||||
cjsRequire, undef;
|
||||
var promisePrototype, makePromisePrototype, reduceArray, slice, fcall, nextTick, handlerQueue,
|
||||
funcProto, call, arrayProto, monitorApi,
|
||||
capturedSetTimeout, cjsRequire, MutationObs, undef;
|
||||
|
||||
cjsRequire = require;
|
||||
|
||||
@ -992,39 +1047,39 @@ define(function (require) {
|
||||
* processing until it is truly empty.
|
||||
*/
|
||||
function drainQueue() {
|
||||
var task, i = 0;
|
||||
|
||||
while(task = handlerQueue[i++]) {
|
||||
task();
|
||||
}
|
||||
|
||||
runHandlers(handlerQueue);
|
||||
handlerQueue = [];
|
||||
}
|
||||
|
||||
// capture setTimeout to avoid being caught by fake timers
|
||||
// used in time based tests
|
||||
setTimeout = global.setTimeout;
|
||||
|
||||
// Allow attaching the monitor to when() if env has no console
|
||||
monitorApi = typeof console != 'undefined' ? console : when;
|
||||
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) {
|
||||
// Sniff "best" async scheduling option
|
||||
// Prefer process.nextTick or MutationObserver, then check for
|
||||
// vertx and finally fall back to setTimeout
|
||||
/*global process,document,setTimeout,MutationObserver,WebKitMutationObserver*/
|
||||
if (typeof process === 'object' && process.nextTick) {
|
||||
nextTick = process.nextTick;
|
||||
} else if(MutationObs =
|
||||
(typeof MutationObserver === 'function' && MutationObserver) ||
|
||||
(typeof WebKitMutationObserver === 'function' && WebKitMutationObserver)) {
|
||||
nextTick = (function(document, MutationObserver, drainQueue) {
|
||||
var el = document.createElement('div');
|
||||
new MutationObserver(drainQueue).observe(el, { attributes: true });
|
||||
|
||||
return function() {
|
||||
el.setAttribute('x', 'x');
|
||||
};
|
||||
}(document, MutationObs, drainQueue));
|
||||
} else {
|
||||
try {
|
||||
// vert.x 1.x || 2.x
|
||||
nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext;
|
||||
} catch(ignore) {
|
||||
nextTick = function(t) { setTimeout(t, 0); };
|
||||
// capture setTimeout to avoid being caught by fake timers
|
||||
// used in time based tests
|
||||
capturedSetTimeout = setTimeout;
|
||||
nextTick = function(t) { capturedSetTimeout(t, 0); };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1095,15 +1150,28 @@ define(function (require) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function crash(fatalError) {
|
||||
if(typeof monitorApi.reportUnhandled === 'function') {
|
||||
monitorApi.reportUnhandled();
|
||||
} else {
|
||||
enqueue(function() {
|
||||
throw fatalError;
|
||||
});
|
||||
}
|
||||
|
||||
throw fatalError;
|
||||
}
|
||||
|
||||
return when;
|
||||
});
|
||||
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this);
|
||||
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); });
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var bane = require("bane");
|
||||
var websocket = require("faye-websocket");
|
||||
var when = require("when");
|
||||
}
|
||||
},{"__browserify_process":3}],5:[function(require,module,exports){
|
||||
/*global module:true, require:false*/
|
||||
|
||||
var bane = require("bane");
|
||||
var websocket = require("../lib/websocket/");
|
||||
var when = require("when");
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
@ -1125,11 +1193,7 @@ function Mopidy(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
} else {
|
||||
Mopidy.WebSocket = window.WebSocket;
|
||||
}
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
var currentHost = (typeof document !== "undefined" &&
|
||||
@ -1394,6 +1458,8 @@ Mopidy.prototype._snakeToCamel = function (name) {
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof exports === "object") {
|
||||
exports.Mopidy = Mopidy;
|
||||
}
|
||||
module.exports = Mopidy;
|
||||
|
||||
},{"../lib/websocket/":1,"bane":2,"when":4}]},{},[5])
|
||||
(5)
|
||||
});
|
||||
6
mopidy/http/data/mopidy.min.js
vendored
6
mopidy/http/data/mopidy.min.js
vendored
File diff suppressed because one or more lines are too long
@ -62,6 +62,12 @@ class MpdUnknownCommand(MpdAckError):
|
||||
self.command = ''
|
||||
|
||||
|
||||
class MpdNoCommand(MpdUnknownCommand):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdNoCommand, self).__init__(*args, **kwargs)
|
||||
self.message = 'No command given'
|
||||
|
||||
|
||||
class MpdNoExistError(MpdAckError):
|
||||
error_code = MpdAckError.ACK_ERROR_NO_EXIST
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.mpd.protocol import handle_request
|
||||
from mopidy.mpd.exceptions import MpdNoCommand
|
||||
|
||||
|
||||
@handle_request(r'[\ ]*$')
|
||||
def empty(context):
|
||||
"""The original MPD server returns ``OK`` on an empty request."""
|
||||
pass
|
||||
"""The original MPD server returns an error on an empty request."""
|
||||
raise MpdNoCommand
|
||||
|
||||
1
setup.py
1
setup.py
@ -43,7 +43,6 @@ setup(
|
||||
'mopidy.ext': [
|
||||
'http = mopidy.http:Extension [http]',
|
||||
'local = mopidy.backends.local:Extension',
|
||||
'local-json = mopidy.backends.local.json:Extension',
|
||||
'mpd = mopidy.mpd:Extension',
|
||||
'stream = mopidy.backends.stream:Extension',
|
||||
],
|
||||
|
||||
@ -17,7 +17,9 @@ class LocalBackendEventsTest(unittest.TestCase):
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'playlists_dir': b'',
|
||||
'library': 'json',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.backends.local.json import actor
|
||||
from mopidy.backends.local import actor, json
|
||||
from mopidy.models import Track, Album, Artist
|
||||
|
||||
from tests import path_to_data_dir
|
||||
@ -61,21 +62,22 @@ class LocalLibraryProviderTest(unittest.TestCase):
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'playlists_dir': b'',
|
||||
},
|
||||
'local-json': {
|
||||
'json_file': path_to_data_dir('library.json.gz'),
|
||||
'library': 'json',
|
||||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.backend = actor.LocalJsonBackend.start(
|
||||
actor.LocalBackend.libraries = [json.JsonLibrary]
|
||||
self.backend = actor.LocalBackend.start(
|
||||
config=self.config, audio=None).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
self.library = self.core.library
|
||||
|
||||
def tearDown(self):
|
||||
pykka.ActorRegistry.stop_all()
|
||||
actor.LocalBackend.libraries = []
|
||||
|
||||
def test_refresh(self):
|
||||
self.library.refresh()
|
||||
@ -88,28 +90,30 @@ class LocalLibraryProviderTest(unittest.TestCase):
|
||||
# Verifies that https://github.com/mopidy/mopidy/issues/500
|
||||
# has been fixed.
|
||||
|
||||
with tempfile.NamedTemporaryFile() as library:
|
||||
with open(self.config['local-json']['json_file']) as fh:
|
||||
library.write(fh.read())
|
||||
library.flush()
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
tmplib = os.path.join(tmpdir, 'library.json.gz')
|
||||
shutil.copy(path_to_data_dir('library.json.gz'), tmplib)
|
||||
|
||||
config = copy.deepcopy(self.config)
|
||||
config['local-json']['json_file'] = library.name
|
||||
backend = actor.LocalJsonBackend(config=config, audio=None)
|
||||
config = {'local': self.config['local'].copy()}
|
||||
config['local']['data_dir'] = tmpdir
|
||||
backend = actor.LocalBackend(config=config, audio=None)
|
||||
|
||||
# Sanity check that value is in the library
|
||||
result = backend.library.lookup(self.tracks[0].uri)
|
||||
self.assertEqual(result, self.tracks[0:1])
|
||||
|
||||
# Clear library and refresh
|
||||
library.seek(0)
|
||||
library.truncate()
|
||||
# Clear and refresh.
|
||||
open(tmplib, 'w').close()
|
||||
backend.library.refresh()
|
||||
|
||||
# Now it should be gone.
|
||||
result = backend.library.lookup(self.tracks[0].uri)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_browse(self):
|
||||
pass # TODO
|
||||
|
||||
@ -22,7 +22,9 @@ class LocalPlaybackProviderTest(unittest.TestCase):
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'playlists_dir': b'',
|
||||
'library': 'json',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'library': 'json',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,9 @@ class LocalTracklistProviderTest(unittest.TestCase):
|
||||
config = {
|
||||
'local': {
|
||||
'media_dir': path_to_data_dir(''),
|
||||
'data_dir': path_to_data_dir(''),
|
||||
'playlists_dir': b'',
|
||||
'library': 'json',
|
||||
}
|
||||
}
|
||||
tracks = [
|
||||
|
||||
@ -13,9 +13,11 @@ class CoreActorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.backend1 = mock.Mock()
|
||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
||||
self.backend1.actor_ref.actor_class.__name__ = b'B1'
|
||||
|
||||
self.backend2 = mock.Mock()
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy2']
|
||||
self.backend2.actor_ref.actor_class.__name__ = b'B2'
|
||||
|
||||
self.core = Core(audio=None, backends=[self.backend1, self.backend2])
|
||||
|
||||
@ -29,32 +31,12 @@ class CoreActorTest(unittest.TestCase):
|
||||
self.assertIn('dummy2', result)
|
||||
|
||||
def test_backends_with_colliding_uri_schemes_fails(self):
|
||||
self.backend1.actor_ref.actor_class.__name__ = b'B1'
|
||||
self.backend2.actor_ref.actor_class.__name__ = b'B2'
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy1', 'dummy2']
|
||||
|
||||
self.assertRaisesRegexp(
|
||||
AssertionError,
|
||||
'Cannot add URI scheme dummy1 for B2, it is already handled by B1',
|
||||
Core, audio=None, backends=[self.backend1, self.backend2])
|
||||
|
||||
def test_backends_with_colliding_uri_schemes_passes(self):
|
||||
"""
|
||||
Checks that backends with overlapping schemes, but distinct sub parts
|
||||
provided can co-exist.
|
||||
"""
|
||||
|
||||
self.backend1.has_library().get.return_value = False
|
||||
self.backend1.has_playlists().get.return_value = False
|
||||
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy1']
|
||||
self.backend2.has_playback().get.return_value = False
|
||||
self.backend2.has_playlists().get.return_value = False
|
||||
|
||||
core = Core(audio=None, backends=[self.backend1, self.backend2])
|
||||
self.assertEqual(core.backends.with_playback,
|
||||
{'dummy1': self.backend1})
|
||||
self.assertEqual(core.backends.with_library,
|
||||
{'dummy1': self.backend2})
|
||||
|
||||
def test_version(self):
|
||||
self.assertEqual(self.core.version, versioning.get_version())
|
||||
|
||||
@ -3,8 +3,8 @@ from __future__ import unicode_literals
|
||||
import unittest
|
||||
|
||||
from mopidy.mpd.exceptions import (
|
||||
MpdAckError, MpdPermissionError, MpdUnknownCommand, MpdSystemError,
|
||||
MpdNotImplemented)
|
||||
MpdAckError, MpdPermissionError, MpdUnknownCommand, MpdNoCommand,
|
||||
MpdSystemError, MpdNotImplemented)
|
||||
|
||||
|
||||
class MpdExceptionsTest(unittest.TestCase):
|
||||
@ -41,6 +41,14 @@ class MpdExceptionsTest(unittest.TestCase):
|
||||
e.get_mpd_ack(),
|
||||
'ACK [5@0] {} unknown command "play"')
|
||||
|
||||
def test_mpd_no_command(self):
|
||||
try:
|
||||
raise MpdNoCommand
|
||||
except MpdAckError as e:
|
||||
self.assertEqual(
|
||||
e.get_mpd_ack(),
|
||||
'ACK [5@0] {} No command given')
|
||||
|
||||
def test_mpd_system_error(self):
|
||||
try:
|
||||
raise MpdSystemError('foo')
|
||||
|
||||
@ -14,10 +14,10 @@ class ConnectionHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
def test_empty_request(self):
|
||||
self.sendRequest('')
|
||||
self.assertEqualResponse('OK')
|
||||
self.assertEqualResponse('ACK [5@0] {} No command given')
|
||||
|
||||
self.sendRequest(' ')
|
||||
self.assertEqualResponse('OK')
|
||||
self.assertEqualResponse('ACK [5@0] {} No command given')
|
||||
|
||||
def test_kill(self):
|
||||
self.sendRequest('kill')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user