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:
Stein Magnus Jodal 2014-01-09 08:39:38 +01:00
commit 1fd1a38013
51 changed files with 1102 additions and 1882 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ node_modules/
nosetests.xml
*~
*.orig
js/test/lib/

View File

@ -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>

View File

@ -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>

View File

@ -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 *

View File

@ -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

View File

@ -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**

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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>`.

View File

@ -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::

View File

@ -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");

View File

@ -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/).

View File

@ -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"]

View File

@ -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 };
});

View File

@ -0,0 +1 @@
module.exports = { Client: window.WebSocket };

View File

@ -0,0 +1,4 @@
{
"browser": "browser.js",
"main": "server.js"
}

View File

@ -0,0 +1 @@
module.exports = require('faye-websocket');

View File

@ -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);

View File

@ -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 = {};
}

View File

@ -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",

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View 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

View File

@ -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]

View File

@ -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)

View File

@ -1,3 +0,0 @@
[local-json]
enabled = true
json_file = $XDG_DATA_DIR/mopidy/local/library.json.gz

View File

@ -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()})

View 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)

View File

@ -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')

View File

@ -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

View File

@ -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()

View File

@ -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)
});

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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

View File

@ -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',
],

View File

@ -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',
}
}

View File

@ -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

View File

@ -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',
}
}

View File

@ -20,6 +20,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase):
config = {
'local': {
'media_dir': path_to_data_dir(''),
'data_dir': path_to_data_dir(''),
'library': 'json',
}
}

View File

@ -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 = [

View File

@ -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())

View File

@ -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')

View File

@ -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')