diff --git a/.gitignore b/.gitignore index 79230110..1ec12cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules/ nosetests.xml *~ *.orig +js/test/lib/ diff --git a/.mailmap b/.mailmap index 2cc42b4c..a427c69c 100644 --- a/.mailmap +++ b/.mailmap @@ -11,3 +11,6 @@ Alexandre Petitjean Javier Domingo Cansino Lasse Bigum Nick Steel +Janez Troha +Luke Giuliani +Colin Montgomerie diff --git a/AUTHORS b/AUTHORS index d3e86ef1..c048b83e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,3 +30,6 @@ - Lasse Bigum - David Eisner - Pål Ruud +- Paul Connolley +- Luke Giuliani +- Colin Montgomerie diff --git a/MANIFEST.in b/MANIFEST.in index b3a70f17..51ba5919 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,23 @@ +include *.py include *.rst +include .coveragerc +include .mailmap +include .travis.yml +include AUTHORS include LICENSE include MANIFEST.in -include data/mopidy.desktop + +recursive-include data * recursive-include docs * prune docs/_build +recursive-include js * +prune js/node_modules +prune js/test/lib + recursive-include mopidy *.conf recursive-include mopidy/http/data * + recursive-include tests *.py recursive-include tests/data * diff --git a/docs/api/http.rst b/docs/api/http.rst index c57597c7..5561955d 100644 --- a/docs/api/http.rst +++ b/docs/api/http.rst @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index 3898e5f3..48c7d822 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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** diff --git a/docs/commands/mopidy.rst b/docs/commands/mopidy.rst index 44e961e6..49c7b5b9 100644 --- a/docs/commands/mopidy.rst +++ b/docs/commands/mopidy.rst @@ -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. diff --git a/docs/ext/local.rst b/docs/ext/local.rst index 0c16142c..aaaeb8e4 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -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. diff --git a/docs/glossary.rst b/docs/glossary.rst index 2acb9981..19c799d4 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -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 `, :term:`frontends `, or GStreamer elements like :term:`mixers `. See :ref:`ext` for a list of existing extensions and :ref:`extensiondev` for how to make a new diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 456ae73a..fb3de75b 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -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 `, and then you're ready to :doc:`run Mopidy `. diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 4bc17a26..4eb25072 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -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:: diff --git a/js/Gruntfile.js b/js/Gruntfile.js index c1e687c9..812ecec4 100644 --- a/js/Gruntfile.js +++ b/js/Gruntfile.js @@ -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"); diff --git a/js/README.md b/js/README.md index 753e858a..5a04cd66 100644 --- a/js/README.md +++ b/js/README.md @@ -35,7 +35,7 @@ Mopidy.js using npm: After npm completes, you can import Mopidy.js using ``require()``: - var Mopidy = require("mopidy").Mopidy; + var Mopidy = require("mopidy"); Using the library @@ -80,3 +80,26 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in `package.json` and thus isn't available through `npm run-script`: PATH=./node_modules/.bin:$PATH grunt foo + + +Changelog +--------- + +### 0.2.0 (2014-01-04) + +- **Backwards incompatible change for Node.js users:** + `var Mopidy = require('mopidy').Mopidy;` must be changed to + `var Mopidy = require('mopidy');` + +- Add support for [Browserify](http://browserify.org/). + +- Upgrade dependencies. + +### 0.1.1 (2013-09-17) + +- Upgrade dependencies. + +### 0.1.0 (2013-03-31) + +- Initial release as a Node.js module to the + [npm registry](https://npmjs.org/). diff --git a/js/buster.js b/js/buster.js index 1cc517c8..c5dec850 100644 --- a/js/buster.js +++ b/js/buster.js @@ -2,23 +2,13 @@ var config = module.exports; config.browser_tests = { environment: "browser", - libs: [ - "lib/bane-*.js", - "lib/when-define-shim.js", - "lib/when-*.js" - ], - sources: ["src/**/*.js"], + libs: ["test/lib/*.js"], testHelpers: ["test/**/*-helper.js"], tests: ["test/**/*-test.js"] }; config.node_tests = { environment: "node", - libs: [ - "lib/bane-*.js", - "lib/when-define-shim.js", - "lib/when-*.js" - ], sources: ["src/**/*.js"], testHelpers: ["test/**/*-helper.js"], tests: ["test/**/*-test.js"] diff --git a/js/lib/bane-1.0.0.js b/js/lib/bane-1.0.0.js deleted file mode 100644 index 8051764d..00000000 --- a/js/lib/bane-1.0.0.js +++ /dev/null @@ -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 }; -}); diff --git a/js/lib/websocket/browser.js b/js/lib/websocket/browser.js new file mode 100644 index 00000000..e594246c --- /dev/null +++ b/js/lib/websocket/browser.js @@ -0,0 +1 @@ +module.exports = { Client: window.WebSocket }; diff --git a/js/lib/websocket/package.json b/js/lib/websocket/package.json new file mode 100644 index 00000000..d1e2ac63 --- /dev/null +++ b/js/lib/websocket/package.json @@ -0,0 +1,4 @@ +{ + "browser": "browser.js", + "main": "server.js" +} diff --git a/js/lib/websocket/server.js b/js/lib/websocket/server.js new file mode 100644 index 00000000..dd24f4be --- /dev/null +++ b/js/lib/websocket/server.js @@ -0,0 +1 @@ +module.exports = require('faye-websocket'); diff --git a/js/lib/when-2.4.0.js b/js/lib/when-2.4.0.js deleted file mode 100644 index aa386275..00000000 --- a/js/lib/when-2.4.0.js +++ /dev/null @@ -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); diff --git a/js/lib/when-define-shim.js b/js/lib/when-define-shim.js deleted file mode 100644 index ad135517..00000000 --- a/js/lib/when-define-shim.js +++ /dev/null @@ -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 = {}; -} diff --git a/js/package.json b/js/package.json index 5b8e46d8..d16cfaa9 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 980256b5..1667f9b1 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -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; diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 0bf97f60..9f2509fc 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -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, diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 2e6a6cc5..1ddd76a4 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -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 diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f797a84d..0c8e3478 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -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) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e9204338..7cb2f0d5 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -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 diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index a9902c8b..c29a5dbe 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -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: diff --git a/mopidy/backends/local/commands.py b/mopidy/backends/local/commands.py index e9eb807c..5e4bfe62 100644 --- a/mopidy/backends/local/commands.py +++ b/mopidy/backends/local/commands.py @@ -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) diff --git a/mopidy/backends/local/ext.conf b/mopidy/backends/local/ext.conf index f906a04f..8f1e860c 100644 --- a/mopidy/backends/local/ext.conf +++ b/mopidy/backends/local/ext.conf @@ -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 diff --git a/mopidy/backends/local/json.py b/mopidy/backends/local/json.py new file mode 100644 index 00000000..7bccf101 --- /dev/null +++ b/mopidy/backends/local/json.py @@ -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 diff --git a/mopidy/backends/local/json/__init__.py b/mopidy/backends/local/json/__init__.py deleted file mode 100644 index 031dae51..00000000 --- a/mopidy/backends/local/json/__init__.py +++ /dev/null @@ -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] diff --git a/mopidy/backends/local/json/actor.py b/mopidy/backends/local/json/actor.py deleted file mode 100644 index 4fc46417..00000000 --- a/mopidy/backends/local/json/actor.py +++ /dev/null @@ -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) diff --git a/mopidy/backends/local/json/ext.conf b/mopidy/backends/local/json/ext.conf deleted file mode 100644 index db0b784a..00000000 --- a/mopidy/backends/local/json/ext.conf +++ /dev/null @@ -1,3 +0,0 @@ -[local-json] -enabled = true -json_file = $XDG_DATA_DIR/mopidy/local/library.json.gz diff --git a/mopidy/backends/local/json/library.py b/mopidy/backends/local/json/library.py deleted file mode 100644 index 5aa0d647..00000000 --- a/mopidy/backends/local/json/library.py +++ /dev/null @@ -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()}) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py new file mode 100644 index 00000000..2d0478ab --- /dev/null +++ b/mopidy/backends/local/library.py @@ -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) diff --git a/mopidy/commands.py b/mopidy/commands.py index 46989c9c..e73f9373 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -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') diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 2055340e..26350f16 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -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 diff --git a/mopidy/ext.py b/mopidy/ext.py index 33b9497d..b29523a7 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -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() diff --git a/mopidy/http/data/mopidy.js b/mopidy/http/data/mopidy.js index 3e4e832e..cc72e3e6 100644 --- a/mopidy/http/data/mopidy.js +++ b/mopidy/http/data/mopidy.js @@ -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 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) +}); \ No newline at end of file diff --git a/mopidy/http/data/mopidy.min.js b/mopidy/http/data/mopidy.min.js index 75d9fff1..450911bd 100644 --- a/mopidy/http/data/mopidy.min.js +++ b/mopidy/http/data/mopidy.min.js @@ -1,5 +1,5 @@ -/*! 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 Mopidy(a){return this instanceof Mopidy?(this._settings=this._configure(a||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(a)}if(("function"==typeof define&&define.amd&&function(a){define("bane",a)}||"object"==typeof module&&function(a){module.exports=a()}||function(a){this.bane=a()})(function(){"use strict";function a(a,b,c){var d,e=c.length;if(e>0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):(d(this,a).push({listener:b(e),thisp:f}),void 0)},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),f.splice(0,f.length),void 0}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return f.splice(0,f.length),void 0;for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return f.splice(h,1),void 0},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f}}),"undefined"!=typeof window&&(window.define=function(a){try{delete window.define}catch(b){window.define=void 0}window.when=a()},window.define.amd={}),function(a,b){"use strict";a(function(a){function c(a,b,c,d){return e(a).then(b,c,d)}function d(a,b){this._message=a,this.inspect=b}function e(a){return h(function(b){b(a)})}function f(a){return c(a,k)}function g(){function a(a,f,g){b.resolve=b.resolver.resolve=function(b){return d?e(b):(d=!0,a(b),c)},b.reject=b.resolver.reject=function(a){return d?e(k(a)):(d=!0,f(a),c)},b.notify=b.resolver.notify=function(a){return g(a),a}}var b,c,d;return b={promise:S,resolve:S,reject:S,notify:S,resolver:{resolve:S,reject:S,notify:S}},b.promise=c=h(a),b}function h(a){return i(a,Q.PromiseStatus&&Q.PromiseStatus())}function i(a,b){function c(a,b,c,d){function e(e){e._message(a,b,c,d)}l?l.push(e):E(function(){e(j)})}function e(){return j?j.inspect():D()}function f(a){l&&(j=n(a),q(l,j),l=S,b&&r(j,b))}function g(a){f(k(a))}function h(a){l&&q(l,m(a))}var i,j,l=[];i=new d(c,e),i._status=b;try{a(f,g,h)}catch(o){g(o)}return i}function j(a){return l(new o(a),function(){return B(a)})}function k(a){return l(new p(a),function(){return C(a)})}function l(a,b){return new d(function(b,c,d){try{d(a[b].apply(a,c))}catch(e){d(k(e))}},b)}function m(a){return new d(function(b,c,d,e){var f=c[2];try{e("function"==typeof f?f(a):a)}catch(g){e(g)}})}function n(a){return a instanceof d?a:a===Object(a)&&"then"in a?h(function(b,c,d){E(function(){try{var e=a.then;"function"==typeof e?J(e,a,b,c,d):b(j(a))}catch(f){c(f)}})}):j(a)}function o(a){this.value=a}function p(a){this.reason=a}function q(a,b){E(function(){for(var c,d=0;c=a[d++];)c(b)})}function r(a,b){function c(){b.fulfilled()}function d(a){b.rejected(a)}a.then(c,d)}function s(a){return a&&"function"==typeof a.then}function t(a,b,d,e,f){return c(a,function(a){function g(d,e,f){function g(a){n(a)}function h(a){m(a)}var i,j,k,l,m,n,o,p;if(o=a.length>>>0,i=Math.max(0,Math.min(b,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=G,e(l))},m=function(a){k.push(a),--i||(m=n=G,d(k))},p=0;o>p;++p)p in a&&c(a[p],h,g,f);else d(k)}return h(g).then(d,e,f)})}function u(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return t(a,1,e,c,d)}function v(a,b,c,d){return z(a,G).then(b,c,d)}function w(){return z(arguments,G)}function x(a){return z(a,B,C)}function y(a,b){return z(a,b)}function z(a,b,d){return c(a,function(a){function e(e,f,g){function h(a,h){c(a,b,d).then(function(a){i[h]=a,g(a),--k||e(i)},f)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return i(e)})}function A(a,b){var d=J(I,arguments,1);return c(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return c(a,function(a){return c(d,function(c){return b(a,c,f,e)})})},H.apply(a,d)})}function B(a){return{state:"fulfilled",value:a}}function C(a){return{state:"rejected",reason:a}}function D(){return{state:"pending"}}function E(a){1===L.push(a)&&K(F)}function F(){for(var a,b=0;a=L[b++];)a();L=[]}function G(a){return a}c.promise=h,c.resolve=e,c.reject=f,c.defer=g,c.join=w,c.all=v,c.map=y,c.reduce=A,c.settle=x,c.any=u,c.some=t,c.isPromise=s,c.isPromiseLike=s,d.prototype={then:function(){var a,b;return a=arguments,b=this._message,i(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(S,a)},ensure:function(a){function b(){return e(a())}return this.then(b,b).yield(this)},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return v(b,function(b){return a.apply(S,b)})})},always:function(a,b){return this.then(a,a,b)}},o.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},p.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var H,I,J,K,L,M,N,O,P,Q,R,S;if(R=a,L=[],M=b.setTimeout,Q="undefined"!=typeof console?console:c,"function"==typeof setImmediate)K=setImmediate.bind(b);else if("undefined"!=typeof MessageChannel){var T=new MessageChannel;T.port1.onmessage=F,K=function(){T.port2.postMessage(0)}}else if("object"==typeof process&&process.nextTick)K=process.nextTick;else try{K=R("vertx").runOnLoop||R("vertx").runOnContext}catch(U){K=function(a){M(a,0)}}return N=Function.prototype,O=N.call,J=N.bind?O.bind(O):function(a,b){return a.apply(b,I.call(arguments,2))},P=[],I=P.slice,H=P.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},c})}("function"==typeof define&&define.amd?define:function(a){module.exports=a(require)},this),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(a){var b="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||"ws://"+b+"/mopidy/ws/",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,a},Mopidy.prototype._getConsole=function(){var a="undefined"!=typeof a&&a||{};return a.log=a.log||function(){},a.warn=a.warn||function(){},a.error=a.error||function(){},a},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},Mopidy.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var c=this._pendingRequests[b];delete this._pendingRequests[b],c.reject({message:"WebSocket closed",closeEvent:a})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},Mopidy.prototype._send=function(a){var b=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:b.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:b.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:b.resolver.reject({message:"WebSocket is closed"});break;default:a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a)}return b.promise},Mopidy.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),Mopidy.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},Mopidy.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return this._console.warn("Unexpected response received. Message was:",a),void 0;var b=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?b.resolve(a.result):a.hasOwnProperty("error")?(b.reject(a.error),this._console.warn("Server returned error:",a.error)):(b.reject({message:"Response without 'result' or 'error' received",data:{response:a}}),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},Mopidy.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(a){var b=function(a){return function(){var b=Array.prototype.slice.call(arguments);return this._send({method:a,params:b})}.bind(this)}.bind(this),c=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},d=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),e=function(e){var f=c(e),g=this._snakeToCamel(f.slice(-1)[0]),h=d(f.slice(0,-1));h[g]=b(e),h[g].description=a[e].description,h[g].params=a[e].params}.bind(this);Object.keys(a).forEach(e),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file +!function(a){if("object"==typeof exports)module.exports=a();else if("function"==typeof define&&define.amd)define(a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.Mopidy=a()}}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};a[g][0].call(j.exports,function(b){var c=a[g][1][b];return e(c?c:b)},j,j.exports,b,a,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):(d(this,a).push({listener:b(e),thisp:f}),void 0)},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),f.splice(0,f.length),void 0}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return f.splice(0,f.length),void 0;for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return f.splice(h,1),void 0},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f,aggregate:function(a){var b=f();return a.forEach(function(a){a.on(function(a,c){b.emit(a,c)})}),b}}})},{}],3:[function(a,b){var c=b.exports={};c.nextTick=function(){var a="undefined"!=typeof window&&window.setImmediate,b="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(a)return function(a){return window.setImmediate(a)};if(b){var c=[];return window.addEventListener("message",function(a){var b=a.source;if((b===window||null===b)&&"process-tick"===a.data&&(a.stopPropagation(),c.length>0)){var d=c.shift();d()}},!0),function(a){c.push(a),window.postMessage("process-tick","*")}}return function(a){setTimeout(a,0)}}(),c.title="browser",c.browser=!0,c.env={},c.argv=[],c.binding=function(){throw new Error("process.binding is not supported")},c.cwd=function(){return"/"},c.chdir=function(){throw new Error("process.chdir is not supported")}},{}],4:[function(b,c){var d=b("__browserify_process");!function(a){"use strict";a(function(a){function b(a,b,c,d){return f(a).then(b,c,d)}function c(a){return new e(a,Q.PromiseStatus&&Q.PromiseStatus())}function e(a,b){function c(){return i?i.inspect():B()}function d(a,b,c,d,e){function f(f){f._when(a,b,c,d,e)}l?l.push(f):C(function(){f(i)})}function e(a){if(l){var c=l;l=U,C(function(){i=k(h,a),b&&p(i,b),j(c,i)})}}function f(a){e(new n(a))}function g(a){if(l){var b=l;C(function(){j(b,new o(a))})}}var h,i,l=[];h=this,this._status=b,this.inspect=c,this._when=d;try{a(e,f,g)}catch(m){f(m)}}function f(a){return a instanceof e?a:g(a)}function g(a){return c(function(b){b(a)})}function h(a){return b(a,function(a){return new n(a)})}function i(){function a(a,c,f){b.resolve=b.resolver.resolve=function(b){return e?g(b):(e=!0,a(b),d)},b.reject=b.resolver.reject=function(a){return e?g(new n(a)):(e=!0,c(a),d)},b.notify=b.resolver.notify=function(a){return f(a),a}}var b,d,e;return b={promise:U,resolve:U,reject:U,notify:U,resolver:{resolve:U,reject:U,notify:U}},b.promise=d=c(a),b}function j(a,b){for(var c=0;c>>0,i=Math.max(0,Math.min(d,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=E,e(l))},m=function(a){k.push(a),--i||(m=n=E,c(k))},p=0;o>p;++p)p in a&&b(a[p],h,g,f);else c(k)}return c(h).then(e,f,g)})}function s(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return r(a,1,e,c,d)}function t(a,b,c,d){return x(a,E).then(b,c,d)}function u(){return x(arguments,E)}function v(a){return x(a,z,A)}function w(a,b){return x(a,b)}function x(a,c,d){return b(a,function(a){function f(e,f,g){function h(a,h){b(a,c,d).then(function(a){i[h]=a,--k||e(i)},f,g)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return new e(f)})}function y(a,c){var d=K(J,arguments,1);return b(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return b(a,function(a){return b(d,function(b){return c(a,b,f,e)})})},I.apply(a,d)})}function z(a){return{state:"fulfilled",value:a}}function A(a){return{state:"rejected",reason:a}}function B(){return{state:"pending"}}function C(a){1===M.push(a)&&L(D)}function D(){j(M),M=[]}function E(a){return a}function F(a){throw"function"==typeof Q.reportUnhandled?Q.reportUnhandled():C(function(){throw a}),a}b.promise=c,b.resolve=g,b.reject=h,b.defer=i,b.join=u,b.all=t,b.map=w,b.reduce=y,b.settle=v,b.any=s,b.some=r,b.isPromise=q,b.isPromiseLike=q,G=e.prototype,G.then=function(a,b,c){var d=this;return new e(function(e,f,g){d._when(e,g,a,b,c)},this._status&&this._status.observed())},G["catch"]=G.otherwise=function(a){return this.then(U,a)},G["finally"]=G.ensure=function(a){function b(){return g(a())}return"function"==typeof a?this.then(b,b).yield(this):this},G.done=function(a,b){this.then(a,b)["catch"](F)},G.yield=function(a){return this.then(function(){return a})},G.tap=function(a){return this.then(a).yield(this)},G.spread=function(a){return this.then(function(b){return t(b,function(b){return a.apply(U,b)})})},G.always=function(a,b){return this.then(a,a,b)},H=Object.create||function(a){function b(){}return b.prototype=a,new b},m.prototype=H(G),m.prototype.inspect=function(){return z(this.value)},m.prototype._when=function(a,b,c){try{a("function"==typeof c?c(this.value):this.value)}catch(d){a(new n(d))}},n.prototype=H(G),n.prototype.inspect=function(){return A(this.value)},n.prototype._when=function(a,b,c,d){try{a("function"==typeof d?d(this.value):this)}catch(e){a(new n(e))}},o.prototype=H(G),o.prototype._when=function(a,b,c,d,e){try{b("function"==typeof e?e(this.value):this.value)}catch(f){b(f)}};var G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U;if(S=a,M=[],Q="undefined"!=typeof console?console:b,"object"==typeof d&&d.nextTick)L=d.nextTick;else if(T="function"==typeof MutationObserver&&MutationObserver||"function"==typeof WebKitMutationObserver&&WebKitMutationObserver)L=function(a,b,c){var d=a.createElement("div");return new b(c).observe(d,{attributes:!0}),function(){d.setAttribute("x","x")}}(document,T,D);else try{L=S("vertx").runOnLoop||S("vertx").runOnContext}catch(V){R=setTimeout,L=function(a){R(a,0)}}return N=Function.prototype,O=N.call,K=N.bind?O.bind(O):function(a,b){return a.apply(b,J.call(arguments,2))},P=[],J=P.slice,I=P.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},b})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{__browserify_process:3}],5:[function(a,b){function c(a){return this instanceof c?(this._settings=this._configure(a||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,d.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new c(a)}var d=a("bane"),e=a("../lib/websocket/"),f=a("when");c.WebSocket=e.Client,c.prototype._configure=function(a){var b="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||"ws://"+b+"/mopidy/ws/",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,a},c.prototype._getConsole=function(){var a="undefined"!=typeof a&&a||{};return a.log=a.log||function(){},a.warn=a.warn||function(){},a.error=a.error||function(){},a},c.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},c.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===c.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new c.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},c.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var c=this._pendingRequests[b];delete this._pendingRequests[b],c.reject({message:"WebSocket closed",closeEvent:a})}.bind(this)),this.emit("state:offline")},c.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},c.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},c.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},c.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},c.prototype._send=function(a){var b=f.defer();switch(this._webSocket.readyState){case c.WebSocket.CONNECTING:b.resolver.reject({message:"WebSocket is still connecting"});break;case c.WebSocket.CLOSING:b.resolver.reject({message:"WebSocket is closing"});break;case c.WebSocket.CLOSED:b.resolver.reject({message:"WebSocket is closed"});break;default:a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a)}return b.promise},c.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),c.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},c.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return this._console.warn("Unexpected response received. Message was:",a),void 0;var b=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?b.resolve(a.result):a.hasOwnProperty("error")?(b.reject(a.error),this._console.warn("Server returned error:",a.error)):(b.reject({message:"Response without 'result' or 'error' received",data:{response:a}}),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},c.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},c.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},c.prototype._createApi=function(a){var b=function(a){return function(){var b=Array.prototype.slice.call(arguments);return this._send({method:a,params:b})}.bind(this)}.bind(this),c=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},d=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),e=function(e){var f=c(e),g=this._snakeToCamel(f.slice(-1)[0]),h=d(f.slice(0,-1));h[g]=b(e),h[g].description=a[e].description,h[g].params=a[e].params}.bind(this);Object.keys(a).forEach(e),this.emit("state:online")},c.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},b.exports=c},{"../lib/websocket/":1,bane:2,when:4}]},{},[5])(5)}); \ No newline at end of file diff --git a/mopidy/mpd/exceptions.py b/mopidy/mpd/exceptions.py index db3212d8..07e3a421 100644 --- a/mopidy/mpd/exceptions.py +++ b/mopidy/mpd/exceptions.py @@ -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 diff --git a/mopidy/mpd/protocol/empty.py b/mopidy/mpd/protocol/empty.py index 9b3d6883..9cb0aa6b 100644 --- a/mopidy/mpd/protocol/empty.py +++ b/mopidy/mpd/protocol/empty.py @@ -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 diff --git a/setup.py b/setup.py index b6857d4e..607496b7 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ setup( 'mopidy.ext': [ 'http = mopidy.http:Extension [http]', 'local = mopidy.backends.local:Extension', - 'local-json = mopidy.backends.local.json:Extension', 'mpd = mopidy.mpd:Extension', 'stream = mopidy.backends.stream:Extension', ], diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py index 1e26a68c..967d4cdb 100644 --- a/tests/backends/local/events_test.py +++ b/tests/backends/local/events_test.py @@ -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', } } diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 92d615ba..b9292f1f 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -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 diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 4c3dd70d..7d48cfea 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -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', } } diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index c02e1d23..6c602282 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -20,6 +20,8 @@ class LocalPlaylistsProviderTest(unittest.TestCase): config = { 'local': { 'media_dir': path_to_data_dir(''), + 'data_dir': path_to_data_dir(''), + 'library': 'json', } } diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index c7cfe51f..28def50c 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -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 = [ diff --git a/tests/core/actor_test.py b/tests/core/actor_test.py index e9e5f396..4a808cad 100644 --- a/tests/core/actor_test.py +++ b/tests/core/actor_test.py @@ -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()) diff --git a/tests/mpd/exception_test.py b/tests/mpd/exception_test.py index ae59253e..b470ed44 100644 --- a/tests/mpd/exception_test.py +++ b/tests/mpd/exception_test.py @@ -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') diff --git a/tests/mpd/protocol/connection_test.py b/tests/mpd/protocol/connection_test.py index 452a2147..34cce6a0 100644 --- a/tests/mpd/protocol/connection_test.py +++ b/tests/mpd/protocol/connection_test.py @@ -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')