Release v0.19.0

This commit is contained in:
Stein Magnus Jodal 2014-07-21 01:47:37 +02:00
commit e12b507568
181 changed files with 7377 additions and 4157 deletions

7
.gitignore vendored
View File

@ -1,6 +1,8 @@
*.egg-info
*.orig
*.pyc
*.swp
*~
.coverage
.idea
.noseids
@ -11,9 +13,8 @@ cover/
coverage.xml
dist/
docs/_build/
js/test/lib/
mopidy.log*
node_modules/
nosetests.xml
*~
*.orig
js/test/lib/
xunit-*.xml

View File

@ -12,5 +12,6 @@ Javier Domingo Cansino <javierdo1@gmail.com> <javier.domingo@fon.com>
Lasse Bigum <lasse@bigum.org> <l.bigum@samsung.com>
Nick Steel <kingosticks@gmail.com> <kingosticks@gmail.com>
Janez Troha <janez.troha@gmail.com> <dz0ny@users.noreply.github.com>
Janez Troha <janez.troha@gmail.com> <dz0ny@ubuntu.si>
Luke Giuliani <luke@giuliani.com.au>
Colin Montgomerie <kiteflyingmonkey@gmail.com>

View File

@ -1,21 +1,25 @@
language: python
python:
- "2.7_with_system_site_packages"
env:
- TOX_ENV=py27
- TOX_ENV=docs
- TOX_ENV=flake8
install:
- "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- "sudo apt-get update || true"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy|python:any/ {print $2}')"
- "pip install coveralls flake8"
before_script:
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
- "sudo apt-get install mopidy graphviz-dev"
- "pip install tox"
script:
- "flake8 $(find . -iname '*.py')"
- "nosetests --with-coverage --cover-package=mopidy"
- "tox -e $TOX_ENV"
after_success:
- "coveralls"
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"
notifications:
irc:

View File

@ -38,3 +38,5 @@
- Arnaud Barisain-Monrose <abarisain@gmail.com>
- nathanharper <nathan.sam.harper@gmail.com>
- Pierpaolo Frasa <pfrasa@smail.uni-koeln.de>
- Thomas Scholtes <thomas-scholtes@gmx.de>
- Sam Willcocks <sam@wlcx.cc>

View File

@ -1,11 +1,13 @@
include *.py
include *.rst
include *.txt
include .coveragerc
include .mailmap
include .travis.yml
include AUTHORS
include LICENSE
include MANIFEST.in
include tox.ini
recursive-include data *

View File

@ -19,24 +19,24 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
- `Source code <https://github.com/mopidy/mopidy>`_
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
- `Download development snapshot <https://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
- `Download development snapshot <https://github.com/mopidy/mopidy/archive/develop.tar.gz#egg=mopidy-dev>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
.. image:: https://pypip.in/v/Mopidy/badge.png
.. image:: https://img.shields.io/pypi/v/Mopidy.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy/
:alt: Latest PyPI version
.. image:: https://pypip.in/d/Mopidy/badge.png
.. image:: https://img.shields.io/pypi/dm/Mopidy.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy/
:alt: Number of PyPI downloads
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
.. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat
:target: https://travis-ci.org/mopidy/mopidy
:alt: Travis CI build status
.. image:: https://coveralls.io/repos/mopidy/mopidy/badge.png?branch=develop
.. image:: https://img.shields.io/coveralls/mopidy/mopidy/develop.svg?style=flat
:target: https://coveralls.io/r/mopidy/mopidy?branch=develop
:alt: Test coverage

28
dev-requirements.txt Normal file
View File

@ -0,0 +1,28 @@
# Automate tasks
fabric
# Build documentation
sphinx
# Check code style, errors, etc
flake8
flake8-import-order
# Mock dependencies in tests
mock
# Test runners
nose
tox
# Measure test's code coverage
coverage
# Check that MANIFEST.in matches Git repo contents before making a release
check-manifest
# To make wheel packages
wheel
# Securely upload packages to PyPI
twine

View File

@ -93,27 +93,7 @@ Backend listener
:members:
.. _backend-implementations:
Backend implementations
=======================
- `Mopidy-Beets <https://github.com/mopidy/mopidy-beets>`_
- `Mopidy-GMusic <https://github.com/hechtus/mopidy-gmusic>`_
- :ref:`ext-local`
- `Mopidy-radio-de <https://github.com/hechtus/mopidy-radio-de>`_
- `Mopidy-SomaFM <https://github.com/AlexandrePTJ/mopidy-somafm>`_
- `Mopidy-SoundCloud <https://github.com/mopidy/mopidy-soundcloud>`_
- `Mopidy-Spotify <https://github.com/mopidy/mopidy-spotify>`_
- :ref:`ext-stream`
- `Mopidy-Subsonic <https://github.com/rattboi/mopidy-subsonic>`_
- `Mopidy-VKontakte <https://github.com/sibuser/mopidy-vkontakte>`_
See :ref:`ext-backends`.

View File

@ -29,12 +29,12 @@ The following requirements applies to any frontend implementation:
- The main actor MUST be able to start and stop the frontend when the main
actor is started and stopped.
- The frontend MAY require additional settings to be set for it to
work.
- The frontend MAY require additional config values to be set for it to work.
- Such settings MUST be documented.
- Such config values MUST be documented.
- The main actor MUST stop itself if the defined settings are not adequate for
- The main actor MUST raise the :exc:`mopidy.exceptions.FrontendError` with a
descriptive error message if the defined config values are not adequate for
the frontend to work properly.
- Any actor which is part of the frontend MAY implement the
@ -42,17 +42,7 @@ The following requirements applies to any frontend implementation:
specified events.
.. _frontend-implementations:
Frontend implementations
========================
- :ref:`ext-http`
- :ref:`ext-mpd`
- `Mopidy-MPRIS <https://github.com/mopidy/mopidy-mpris>`_
- `Mopidy-Notifier <https://github.com/sauberfred/mopidy-notifier>`_
- `Mopidy-Scrobbler <https://github.com/mopidy/mopidy-scrobbler>`_
See :ref:`ext-frontends`.

195
docs/api/http-server.rst Normal file
View File

@ -0,0 +1,195 @@
.. _http-server-api:
********************
HTTP server side API
********************
The :ref:`ext-http` extension comes with an HTTP server to host Mopidy's
:ref:`http-api`. This web server can also be used by other extensions that need
to expose something over HTTP.
The HTTP server side API can be used to:
- host static files for e.g. a Mopidy client written in pure JavaScript,
- host a `Tornado <http://www.tornadoweb.org/>`__ application, or
- host a WSGI application, including e.g. Flask applications.
To host static files using the web server, an extension needs to register a
name and a file path in the extension registry under the ``http:static`` key.
To extend the web server with a web application, an extension must register a
name and a factory function in the extension registry under the ``http:app``
key.
For details on how to make a Mopidy extension, see the :ref:`extensiondev`
guide.
Static web client example
=========================
To serve static files, you just need to register an ``http:static`` dictionary
in the extension registry. The dictionary must have two keys: ``name`` and
``path``. The ``name`` is used to build the URL the static files will be
served on. By convention, it should be identical with the extension's
:attr:`~mopidy.ext.Extension.ext_name`, like in the following example. The
``path`` tells Mopidy where on the disk the static files are located.
Assuming that the code below is located in the file
:file:`mywebclient/__init__.py`, the files in the directory
:file:`mywebclient/static/` will be made available at ``/mywebclient/`` on
Mopidy's web server. For example, :file:`mywebclient/static/foo.html` will be
available at http://localhost:6680/mywebclient/foo.html.
::
from __future__ import unicode_literals
import os
from mopidy import ext
class MyWebClientExtension(ext.Extension):
ext_name = 'mywebclient'
def setup(self, registry):
registry.add('http:static', {
'name': self.ext_name,
'path': os.path.join(os.path.dirname(__file__), 'static'),
})
# See the Extension API for the full details on this class
Tornado application example
===========================
The :ref:`ext-http` extension's web server is based on the `Tornado
<http://www.tornadoweb.org/>`__ web framework. Thus, it has first class support
for Tornado request handlers.
In the following example, we create a :class:`tornado.web.RequestHandler`
called :class:`MyRequestHandler` that responds to HTTP GET requests with the
string ``Hello, world! This is Mopidy $version``, where it gets the Mopidy
version from Mopidy's core API.
To hook the request handler into Mopidy's web server, we must register a
dictionary under the ``http:app`` key in the extension registry. The
dictionary must have two keys: ``name`` and ``factory``.
The ``name`` is used to build the URL the app will be served on. By convention,
it should be identical with the extension's
:attr:`~mopidy.ext.Extension.ext_name`, like in the following example.
The ``factory`` must be a function that accepts two arguments, ``config`` and
``core``, respectively a dict structure of Mopidy's config and a
:class:`pykka.ActorProxy` to the full Mopidy core API. The ``factory`` function
must return a list of Tornado request handlers. The URL patterns of the request
handlers should not include the ``name``, as that will be prepended to the URL
patterns by the web server.
When the extension is installed, Mopidy will respond to requests to
http://localhost:6680/mywebclient/ with the string ``Hello, world! This is
Mopidy $version``.
::
from __future__ import unicode_literals
import os
import tornado.web
from mopidy import ext
class MyRequestHandler(tornado.web.RequestHandler):
def initialize(self, core):
self.core = core
def get(self):
self.write(
'Hello, world! This is Mopidy %s' %
self.core.get_version().get())
def my_app_factory(config, core):
return [
('/', MyRequestHandler, {'core': core})
]
class MyWebClientExtension(ext.Extension):
ext_name = 'mywebclient'
def setup(self, registry):
registry.add('http:app', {
'name': self.ext_name,
'factory': my_app_factory,
})
# See the Extension API for the full details on this class
WSGI application example
========================
WSGI applications are second-class citizens on Mopidy's HTTP server. The WSGI
applications are run inside Tornado, which is based on non-blocking I/O and a
single event loop. In other words, your WSGI applications will only have a
single thread to run on, and if your application is doing blocking I/O, it will
block all other requests from being handled by the web server as well.
The example below shows how a WSGI application that returns the string
``Hello, world! This is Mopidy $version`` on all requests. The WSGI application
is wrapped as a Tornado application and mounted at
http://localhost:6680/mywebclient/.
::
from __future__ import unicode_literals
import os
import tornado.web
import tornado.wsgi
from mopidy import ext
def my_app_factory(config, core):
def wsgi_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [
'Hello, world! This is Mopidy %s\n' %
self.core.get_version().get()
]
return [
('(.*)', tornado.web.FallbackHandler, {
'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
}),
]
class MyWebClientExtension(ext.Extension):
ext_name = 'mywebclient'
def setup(self, registry):
registry.add('http:app', {
'name': self.ext_name,
'factory': my_app_factory,
})
# See the Extension API for the full details on this class
API implementors
================
See :ref:`ext-web`.

View File

@ -1,18 +1,23 @@
.. _http-api:
********
HTTP API
********
*****************
HTTP JSON-RPC API
*****************
The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available over
HTTP using WebSockets. We also provide a JavaScript wrapper, called
:ref:`Mopidy.js <mopidy-js>` around the HTTP API for use both from browsers and
Node.js.
.. module:: mopidy.http
:synopsis: The HTTP frontend APIs
The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available using
JSON-RPC over HTTP using HTTP POST and WebSockets. We also provide a JavaScript
wrapper, called :ref:`Mopidy.js <mopidy-js>`, around the JSON-RPC over
WebSocket API for use both from browsers and Node.js. The
:ref:`http-explore-extension` extension, can also be used to get you
familiarized with HTTP based APIs.
.. warning:: API stability
Since the HTTP API exposes our internal core API directly it is to be
regarded as **experimental**. We cannot promise to keep any form of
Since the HTTP JSON-RPC API exposes our internal core API directly it is to
be regarded as **experimental**. We cannot promise to keep any form of
backwards compatibility between releases as we will need to change the core
API while working out how to support new use cases. Thus, if you use this
API, you must expect to do small adjustments to your client for every
@ -22,36 +27,50 @@ Node.js.
stable.
.. _http-post-api:
HTTP POST API
=============
The Mopidy web server accepts HTTP requests with the POST method to
http://localhost:6680/mopidy/rpc, where the ``localhost:6680`` part will vary
with your local setup. The HTTP POST endpoint gives you access to Mopidy's
full core API, but does not give you notification on events. If you need
to listen to events, you should probably use the WebSocket API instead.
Example usage from the command line::
$ curl -d '{"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_state"}' http://localhost:6680/mopidy/rpc
{"jsonrpc": "2.0", "id": 1, "result": "stopped"}
For details on the request and response format, see :ref:`json-rpc`.
.. _websocket-api:
WebSocket API
=============
The web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you
access to Mopidy's full API and enables Mopidy to instantly push events to the
client, as they happen.
The Mopidy web server exposes a WebSocket at http://localhost:6680/mopidy/ws,
where the ``localhost:6680`` part will vary with your local setup. The
WebSocket gives you access to Mopidy's full API and enables Mopidy to instantly
push events to the client, as they happen.
On the WebSocket we send two different kind of messages: The client can send
JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses.
In addition, the server will send event messages when something happens on the
server. Both message types are encoded as JSON objects.
:ref:`JSON-RPC 2.0 requests <json-rpc>`, and the server will respond with
JSON-RPC 2.0 responses. In addition, the server will send :ref:`event messages
<json-events>` when something happens on the server. Both message types are
encoded as JSON objects.
If you're using the API from JavaScript, either in the browser or in Node.js,
you should use :ref:`mopidy-js` which wraps the WebSocket API in a nice
JavaScript API.
Event messages
--------------
.. _json-rpc:
Event objects will always have a key named ``event`` whose value is the event
type. Depending on the event type, the event may include additional fields for
related data. The events maps directly to the :class:`mopidy.core.CoreListener`
API. Refer to the ``CoreListener`` method names is the available event types.
The ``CoreListener`` method's keyword arguments are all included as extra
fields on the event objects. Example event message::
{"event": "track_playback_started", "track": {...}}
JSON-RPC 2.0 messaging
----------------------
JSON-RPC 2.0 messages
=====================
JSON-RPC 2.0 messages can be recognized by checking for the key named
``jsonrpc`` with the string value ``2.0``. For details on the messaging format,
@ -80,360 +99,17 @@ available methods. If you're unsure how the core API maps to JSON-RPC, having a
look at the ``core.describe`` response can be helpful.
.. _mopidy-js:
.. _json-events:
Mopidy.js JavaScript library
============================
Event messages
==============
We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets
you quickly started with working on your client instead of figuring out how to
communicate with Mopidy.
Getting the library for browser use
-----------------------------------
Regular and minified versions of Mopidy.js, ready for use, is installed
together with Mopidy. When the HTTP extension is enabled, the files are
available at:
- http://localhost:6680/mopidy/mopidy.js
- http://localhost:6680/mopidy/mopidy.min.js
You may need to adjust hostname and port for your local setup.
Thus, if you use Mopidy to host your web client, like described above, you can
load the latest version of Mopidy.js by adding the following script tag to your
HTML file:
.. code-block:: html
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
If you don't use Mopidy to host your web client, you can find the JS files in
the Git repo at:
- ``mopidy/http/data/mopidy.js``
- ``mopidy/http/data/mopidy.min.js``
Getting the library for Node.js use
-----------------------------------
If you want to use Mopidy.js from Node.js instead of a browser, you can install
Mopidy.js using npm::
npm install mopidy
After npm completes, you can import Mopidy.js using ``require()``:
.. code-block:: js
var Mopidy = require("mopidy");
Getting the library for development on the library
--------------------------------------------------
If you want to work on the Mopidy.js library itself, you'll find a complete
development setup in the ``js/`` dir in our repo. The instructions in
``js/README.md`` will guide you on your way.
Creating an instance
--------------------
Once you got Mopidy.js loaded, you need to create an instance of the wrapper:
.. code-block:: js
var mopidy = new Mopidy();
When you instantiate ``Mopidy()`` without arguments, it will connect to
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
your web client using Mopidy's web server, or if you use Mopidy.js from a
Node.js environment, you'll need to pass the URL to the WebSocket end point:
.. code-block:: js
var mopidy = new Mopidy({
webSocketUrl: "ws://localhost:6680/mopidy/ws/"
});
It is also possible to create an instance first and connect to the WebSocket
later:
.. code-block:: js
var mopidy = new Mopidy({autoConnect: false});
// ... do other stuff, like hooking up events ...
mopidy.connect();
Hooking up to events
--------------------
Once you have a Mopidy.js object, you can hook up to the events it emits. To
explore your possibilities, it can be useful to subscribe to all events and log
them:
.. code-block:: js
mopidy.on(console.log.bind(console));
Several types of events are emitted:
- You can get notified about when the Mopidy.js object is connected to the
server and ready for method calls, when it's offline, and when it's trying to
reconnect to the server by looking at the events ``state:online``,
``state:offline``, ``reconnectionPending``, and ``reconnecting``.
- You can get events sent from the Mopidy server by looking at the events with
the name prefix ``event:``, like ``event:trackPlaybackStarted``.
- You can introspect what happens internally on the WebSocket by looking at the
events emitted with the name prefix ``websocket:``.
Mopidy.js uses the event emitter library `BANE
<https://github.com/busterjs/bane>`_, so you should refer to BANE's
short API documentation to see how you can hook up your listeners to the
different events.
Calling core API methods
------------------------
Once your Mopidy.js object has connected to the Mopidy server and emits the
``state:online`` event, it is ready to accept core API method calls:
.. code-block:: js
mopidy.on("state:online", function () {
mopidy.playback.next();
});
Any calls you make before the ``state:online`` event is emitted will fail. If
you've hooked up an errback (more on that a bit later) to the promise returned
from the call, the errback will be called with an error message.
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
API attributes is *not* available, but that shouldn't be a problem as we've
added (undocumented) getters and setters for all of them, so you can access the
attributes as well from JavaScript.
Both the WebSocket API and the JavaScript API are based on introspection of the
core Python API. Thus, they will always be up to date and immediately reflect
any changes we do to the core API.
The best way to explore the JavaScript API, is probably by opening your
browser's console, and using its tab completion to navigate the API. You'll
find the Mopidy core API exposed under ``mopidy.playback``,
``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``.
All methods in the JavaScript API have an associated data structure describing
the Python params it expects, and most methods also have the Python API
documentation available. This is available right there in the browser console,
by looking at the method's ``description`` and ``params`` attributes:
.. code-block:: js
console.log(mopidy.playback.next.params);
console.log(mopidy.playback.next.description);
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
by-name. Combinations of both, like we're used to from Python, isn't supported
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
passing parameters by-position.
Obviously, you'll want to get a return value from many of your method calls.
Since everything is happening across the WebSocket and maybe even across the
network, you'll get the results asynchronously. Instead of having to pass
callbacks and errbacks to every method you call, the methods return "promise"
objects, which you can use to pipe the future result as input to another
method, or to hook up callback and errback functions.
.. code-block:: js
var track = mopidy.playback.getCurrentTrack();
// => ``track`` isn't a track, but a "promise" object
Instead, typical usage will look like this:
.. code-block:: js
var printCurrentTrack = function (track) {
if (track) {
console.log("Currently playing:", track.name, "by",
track.artists[0].name, "from", track.album.name);
} else {
console.log("No current track");
}
};
mopidy.playback.getCurrentTrack().then(
printCurrentTrack, console.error.bind(console));
The first function passed to ``then()``, ``printCurrentTrack``, is the callback
that will be called if the method call succeeds. The second function,
``console.error``, is the errback that will be called if anything goes wrong.
If you don't hook up an errback, debugging will be hard as errors will silently
go missing.
For debugging, you may be interested in errors from function without
interesting return values as well. In that case, you can pass ``null`` as the
callback:
.. code-block:: js
mopidy.playback.next().then(null, console.error.bind(console));
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
implementation known as `when.js <https://github.com/cujojs/when>`_. Please
refer to when.js' documentation or the standard for further details on how to
work with promise objects.
Cleaning up
-----------
If you for some reason want to clean up after Mopidy.js before the web page is
closed or navigated away from, you can close the WebSocket, unregister all
event listeners, and delete the object like this:
.. code-block:: js
// Close the WebSocket without reconnecting. Letting the object be garbage
// collected will have the same effect, so this isn't strictly necessary.
mopidy.close();
// Unregister all event listeners. If you don't do this, you may have
// lingering references to the object causing the garbage collector to not
// clean up after it.
mopidy.off();
// Delete your reference to the object, so it can be garbage collected.
mopidy = null;
Example to get started with
---------------------------
1. Make sure that you've installed all dependencies required by
:ref:`ext-http`.
2. Create an empty directory for your web client.
3. Change the :confval:`http/static_dir` config value to point to your new
directory.
4. Start/restart Mopidy.
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
world!".
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
there.
7. Include Mopidy.js in your web page:
.. code-block:: html
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
8. Add one of the following Mopidy.js examples of how to queue and start
playback of your first playlist either to your web page or a JavaScript file
that you include in your web page.
"Imperative" style:
.. code-block:: js
var consoleError = console.error.bind(console);
var trackDesc = function (track) {
return track.name + " by " + track.artists[0].name +
" from " + track.album.name;
};
var queueAndPlayFirstPlaylist = function () {
mopidy.playlists.getPlaylists().then(function (playlists) {
var playlist = playlists[0];
console.log("Loading playlist:", playlist.name);
mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) {
mopidy.playback.play(tlTracks[0]).then(function () {
mopidy.playback.getCurrentTrack().then(function (track) {
console.log("Now playing:", trackDesc(track));
}, consoleError);
}, consoleError);
}, consoleError);
}, consoleError);
};
var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlayFirstPlaylist);
Approximately the same behavior in a more functional style, using chaining
of promisies.
.. code-block:: js
var consoleError = console.error.bind(console);
var getFirst = function (list) {
return list[0];
};
var extractTracks = function (playlist) {
return playlist.tracks;
};
var printTypeAndName = function (model) {
console.log(model.__model__ + ": " + model.name);
// By returning the playlist, this function can be inserted
// anywhere a model with a name is piped in the chain.
return model;
};
var trackDesc = function (track) {
return track.name + " by " + track.artists[0].name +
" from " + track.album.name;
};
var printNowPlaying = function () {
// By returning any arguments we get, the function can be inserted
// anywhere in the chain.
var args = arguments;
return mopidy.playback.getCurrentTrack().then(function (track) {
console.log("Now playing:", trackDesc(track));
return args;
});
};
var queueAndPlayFirstPlaylist = function () {
mopidy.playlists.getPlaylists()
// => list of Playlists
.then(getFirst, consoleError)
// => Playlist
.then(printTypeAndName, consoleError)
// => Playlist
.then(extractTracks, consoleError)
// => list of Tracks
.then(mopidy.tracklist.add, consoleError)
// => list of TlTracks
.then(getFirst, consoleError)
// => TlTrack
.then(mopidy.playback.play, consoleError)
// => null
.then(printNowPlaying, consoleError);
};
var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlayFirstPlaylist);
9. The web page should now queue and play your first playlist every time your
load it. See the browser's console for output from the function, any errors,
and all events that are emitted.
Event objects will always have a key named ``event`` whose value is the event
type. Depending on the event type, the event may include additional fields for
related data. The events maps directly to the :class:`mopidy.core.CoreListener`
API. Refer to the :class:`~mopidy.core.CoreListener` method names is the
available event types. The :class:`~mopidy.core.CoreListener` method's keyword
arguments are all included as extra fields on the event objects. Example event
message::
{"event": "track_playback_started", "track": {...}}

View File

@ -21,9 +21,12 @@ API reference
backends
core
audio
mixer
frontends
commands
ext
config
zeroconf
http-server
http
js

440
docs/api/js.rst Normal file
View File

@ -0,0 +1,440 @@
.. _mopidy-js:
****************************
Mopidy.js JavaScript library
****************************
We've made a JavaScript library, Mopidy.js, which wraps the
:ref:`websocket-api` and gets you quickly started with working on your client
instead of figuring out how to communicate with Mopidy.
.. warning:: API stability
Since the Mopidy.js API exposes our internal core API directly it is to be
regarded as **experimental**. We cannot promise to keep any form of
backwards compatibility between releases as we will need to change the core
API while working out how to support new use cases. Thus, if you use this
API, you must expect to do small adjustments to your client for every
release of Mopidy.
From Mopidy 1.0 and onwards, we intend to keep the core API far more
stable.
Getting the library for browser use
===================================
Regular and minified versions of Mopidy.js, ready for use, is installed
together with Mopidy. When the HTTP extension is enabled, the files are
available at:
- http://localhost:6680/mopidy/mopidy.js
- http://localhost:6680/mopidy/mopidy.min.js
You may need to adjust hostname and port for your local setup.
Thus, if you use Mopidy to host your web client, like described above, you can
load the latest version of Mopidy.js by adding the following script tag to your
HTML file:
.. code-block:: html
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
If you don't use Mopidy to host your web client, you can find the JS files in
the Git repo at:
- ``mopidy/http/data/mopidy.js``
- ``mopidy/http/data/mopidy.min.js``
Getting the library for Node.js or Browserify use
=================================================
If you want to use Mopidy.js from Node.js or on the web through Browserify, you
can install Mopidy.js using npm::
npm install mopidy
After npm completes, you can import Mopidy.js using ``require()``:
.. code-block:: js
var Mopidy = require("mopidy");
Getting the library for development on the library
==================================================
If you want to work on the Mopidy.js library itself, you'll find a complete
development setup in the ``js/`` dir in our repo. The instructions in
``js/README.md`` will guide you on your way.
Creating an instance
====================
Once you have Mopidy.js loaded, you need to create an instance of the wrapper:
.. code-block:: js
var mopidy = new Mopidy();
When you instantiate ``Mopidy()`` without arguments, it will connect to
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
your web client using Mopidy's web server, or if you use Mopidy.js from a
Node.js environment, you'll need to pass the URL to the WebSocket end point:
.. code-block:: js
var mopidy = new Mopidy({
webSocketUrl: "ws://localhost:6680/mopidy/ws/"
});
It is also possible to create an instance first and connect to the WebSocket
later:
.. code-block:: js
var mopidy = new Mopidy({autoConnect: false});
// ... do other stuff, like hooking up events ...
mopidy.connect();
When creating an instance, you can specify the following settings:
``autoConnect``
Whether or not to connect to the WebSocket on instance creation. Defaults
to true.
``backoffDelayMin``
The minimum number of milliseconds to wait after a connection error before
we try to reconnect. For every failed attempt, the backoff delay is doubled
until it reaches ``backoffDelayMax``. Defaults to 1000.
``backoffDelayMax``
The maximum number of milliseconds to wait after a connection error before
we try to reconnect. Defaults to 64000.
``callingConvention``
Which calling convention to use when calling methods.
If set to "by-position-only", methods expect to be called with positional
arguments, like ``mopidy.foo.bar(null, true, 2)``.
If set to "by-position-or-by-name", methods expect to be called either with
an array of position arguments, like ``mopidy.foo.bar([null, true, 2])``,
or with an object of named arguments, like ``mopidy.foo.bar({id: 2})``. The
advantage of the "by-position-or-by-name" calling convention is that
arguments with default values can be left out of the named argument object.
Using named arguments also makes the code more readable, and more resistent
to future API changes.
.. note::
For backwards compatibility, the default is "by-position-only". In the
future, the default will change to "by-position-or-by-name". You should
explicitly set this setting to your choice, so you won't be affected
when the default changes.
.. versionadded:: 0.19 (Mopidy.js 0.4)
``console``
If set, this object will be used to log errors from Mopidy.js. This is
mostly useful for testing Mopidy.js.
``webSocket``
An existing WebSocket object to be used instead of creating a new
WebSocket. Defaults to undefined.
``webSocketUrl``
URL used when creating new WebSocket objects. Defaults to
``ws://<document.location.host>/mopidy/ws``, or
``ws://localhost/mopidy/ws`` if ``document.location.host`` isn't
available, like it is in the browser environment.
Hooking up to events
====================
Once you have a Mopidy.js object, you can hook up to the events it emits. To
explore your possibilities, it can be useful to subscribe to all events and log
them:
.. code-block:: js
mopidy.on(console.log.bind(console));
Several types of events are emitted:
- You can get notified about when the Mopidy.js object is connected to the
server and ready for method calls, when it's offline, and when it's trying to
reconnect to the server by looking at the events ``state:online``,
``state:offline``, ``reconnectionPending``, and ``reconnecting``.
- You can get events sent from the Mopidy server by looking at the events with
the name prefix ``event:``, like ``event:trackPlaybackStarted``.
- You can introspect what happens internally on the WebSocket by looking at the
events emitted with the name prefix ``websocket:``.
Mopidy.js uses the event emitter library `BANE
<https://github.com/busterjs/bane>`_, so you should refer to BANE's
short API documentation to see how you can hook up your listeners to the
different events.
Calling core API methods
========================
Once your Mopidy.js object has connected to the Mopidy server and emits the
``state:online`` event, it is ready to accept core API method calls:
.. code-block:: js
mopidy.on("state:online", function () {
mopidy.playback.next();
});
Any calls you make before the ``state:online`` event is emitted will fail. If
you've hooked up an errback (more on that a bit later) to the promise returned
from the call, the errback will be called with a ``Mopidy.ConnectionError``
instance.
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
API attributes is *not* available, but that shouldn't be a problem as we've
added (undocumented) getters and setters for all of them, so you can access the
attributes as well from JavaScript. For example, the
:attr:`mopidy.core.PlaybackController.state` attribute is available in
JSON-RPC as the method ``core.playback.get_state`` and in Mopidy.js as
``mopidy.playback.getState()``.
Both the WebSocket API and the JavaScript API are based on introspection of the
core Python API. Thus, they will always be up to date and immediately reflect
any changes we do to the core API.
The best way to explore the JavaScript API, is probably by opening your
browser's console, and using its tab completion to navigate the API. You'll
find the Mopidy core API exposed under ``mopidy.playback``,
``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``.
All methods in the JavaScript API have an associated data structure describing
the Python params it expects, and most methods also have the Python API
documentation available. This is available right there in the browser console,
by looking at the method's ``description`` and ``params`` attributes:
.. code-block:: js
console.log(mopidy.playback.next.params);
console.log(mopidy.playback.next.description);
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
by-name. Combinations of both, like we're used to from Python, isn't supported
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
passing parameters by-position.
Obviously, you'll want to get a return value from many of your method calls.
Since everything is happening across the WebSocket and maybe even across the
network, you'll get the results asynchronously. Instead of having to pass
callbacks and errbacks to every method you call, the methods return "promise"
objects, which you can use to pipe the future result as input to another
method, or to hook up callback and errback functions.
.. code-block:: js
var track = mopidy.playback.getCurrentTrack();
// => ``track`` isn't a track, but a "promise" object
Instead, typical usage will look like this:
.. code-block:: js
var printCurrentTrack = function (track) {
if (track) {
console.log("Currently playing:", track.name, "by",
track.artists[0].name, "from", track.album.name);
} else {
console.log("No current track");
}
};
mopidy.playback.getCurrentTrack()
.done(printCurrentTrack);
The function passed to ``done()``, ``printCurrentTrack``, is the callback
that will be called if the method call succeeds. If anything goes wrong,
``done()`` will throw an exception.
If you want to explicitly handle any errors and avoid an exception being
thrown, you can register an error handler function anywhere in a promise
chain. The function will be called with the error object as the only argument:
.. code-block:: js
mopidy.playback.getCurrentTrack()
.catch(console.error.bind(console));
.done(printCurrentTrack);
You can also register the error handler at the end of the promise chain by
passing it as the second argument to ``done()``:
.. code-block:: js
mopidy.playback.getCurrentTrack()
.done(printCurrentTrack, console.error.bind(console));
If you don't hook up an error handler function and never call ``done()`` on the
promise object, when.js will log warnings to the console that you have
unhandled errors. In general, unhandled errors will not go silently missing.
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
implementation known as `when.js <https://github.com/cujojs/when>`_. Please
refer to when.js' documentation or the standard for further details on how to
work with promise objects.
Cleaning up
===========
If you for some reason want to clean up after Mopidy.js before the web page is
closed or navigated away from, you can close the WebSocket, unregister all
event listeners, and delete the object like this:
.. code-block:: js
// Close the WebSocket without reconnecting. Letting the object be garbage
// collected will have the same effect, so this isn't strictly necessary.
mopidy.close();
// Unregister all event listeners. If you don't do this, you may have
// lingering references to the object causing the garbage collector to not
// clean up after it.
mopidy.off();
// Delete your reference to the object, so it can be garbage collected.
mopidy = null;
Example to get started with
===========================
1. Make sure that you've installed all dependencies required by
:ref:`ext-http`.
2. Create an empty directory for your web client.
3. Change the :confval:`http/static_dir` config value to point to your new
directory.
4. Start/restart Mopidy.
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
world!".
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
there.
7. Include Mopidy.js in your web page:
.. code-block:: html
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
8. Add one of the following Mopidy.js examples of how to queue and start
playback of your first playlist either to your web page or a JavaScript file
that you include in your web page.
"Imperative" style:
.. code-block:: js
var trackDesc = function (track) {
return track.name + " by " + track.artists[0].name +
" from " + track.album.name;
};
var queueAndPlay = function (playlistNum, trackNum) {
playlistNum = playlistNum || 0;
trackNum = trackNum || 0;
mopidy.playlists.getPlaylists().then(function (playlists) {
var playlist = playlists[playlistNum];
console.log("Loading playlist:", playlist.name);
return mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) {
return mopidy.playback.play(tlTracks[trackNum]).then(function () {
return mopidy.playback.getCurrentTrack().then(function (track) {
console.log("Now playing:", trackDesc(track));
});
});
});
})
.catch(console.error.bind(console)) // Handle errors here
.done(); // ...or they'll be thrown here
};
var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlay);
Approximately the same behavior in a more functional style, using chaining
of promises.
.. code-block:: js
var get = function (key, object) {
return object[key];
};
var printTypeAndName = function (model) {
console.log(model.__model__ + ": " + model.name);
// By returning the playlist, this function can be inserted
// anywhere a model with a name is piped in the chain.
return model;
};
var trackDesc = function (track) {
return track.name + " by " + track.artists[0].name +
" from " + track.album.name;
};
var printNowPlaying = function () {
// By returning any arguments we get, the function can be inserted
// anywhere in the chain.
var args = arguments;
return mopidy.playback.getCurrentTrack()
.then(function (track) {
console.log("Now playing:", trackDesc(track));
return args;
});
};
var queueAndPlay = function (playlistNum, trackNum) {
playlistNum = playlistNum || 0;
trackNum = trackNum || 0;
mopidy.playlists.getPlaylists()
// => list of Playlists
.fold(get, playlistNum)
// => Playlist
.then(printTypeAndName)
// => Playlist
.fold(get, 'tracks')
// => list of Tracks
.then(mopidy.tracklist.add)
// => list of TlTracks
.fold(get, trackNum)
// => TlTrack
.then(mopidy.playback.play)
// => null
.then(printNowPlaying)
// => null
.catch(console.error.bind(console)) // Handle errors here
// => null
.done(); // ...or they'll be thrown here
};
var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlay);
9. The web page should now queue and play your first playlist every time you
load it. See the browser's console for output from the function, any errors,
and all events that are emitted.

20
docs/api/mixer.rst Normal file
View File

@ -0,0 +1,20 @@
.. _mixer-api:
***************
Audio mixer API
***************
.. module:: mopidy.mixer
:synopsis: The audio mixer API
.. autoclass:: mopidy.mixer.Mixer
:members:
.. autoclass:: mopidy.mixer.MixerListener
:members:
Mixer implementations
=====================
See :ref:`ext-mixers`.

View File

@ -5,6 +5,225 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v0.19.0 (2014-07-21)
====================
The focus of 0.19 have been on improving the MPD implementation, replacing
GStreamer mixers with our own mixer API, and on making web clients installable
with ``pip``, like any other Mopidy extension.
Since the release of 0.18, we've closed or merged 53 issues and pull requests
through about 445 commits by :ref:`12 people <authors>`, including five new
guys. Thanks to everyone that has contributed!
**Dependencies**
- Mopidy now requires Tornado >= 3.1.
- Mopidy no longer requires CherryPy or ws4py. Previously, these were optional
dependencies required for the HTTP frontend to work.
**Backend API**
- *Breaking change:* Imports of the backend API from
:mod:`mopidy.backends` no longer works. The new API introuced in v0.18 is now
required. Most extensions already use the new API location.
**Commands**
- The ``mopidy-convert-config`` tool for migrating the ``setings.py``
configuration file used by Mopidy up until 0.14 to the new config file format
has been removed after over a year of trusty service. If you still need to
convert your old ``settings.py`` configuration file, do so using and older
release, like Mopidy 0.18, or migrate the configuration to the new format by
hand.
**Configuration**
- Add ``optional=True`` support to :class:`mopidy.config.Boolean`.
**Logging**
- Fix proper decoding of exception messages that depends on the user's locale.
- Colorize logs depending on log level. This can be turned off with the new
:confval:`logging/color` configuration. (Fixes: :issue:`772`)
**Extension support**
- *Breaking change:* Removed the :class:`~mopidy.ext.Extension` methods that
were deprecated in 0.18: :meth:`~mopidy.ext.Extension.get_backend_classes`,
:meth:`~mopidy.ext.Extension.get_frontend_classes`, and
:meth:`~mopidy.ext.Extension.register_gstreamer_elements`. Use
:meth:`mopidy.ext.Extension.setup` instead, as most extensions already do.
**Audio**
- *Breaking change:* Removed support for GStreamer mixers. GStreamer 1.x does
not support volume control, so we changed to use software mixing by default
in v0.17.0. Now, we're removing support for all other GStreamer mixers and
are reintroducing mixers as something extensions can provide independently of
GStreamer. (Fixes: :issue:`665`, PR: :issue:`760`)
- *Breaking change:* Changed the :confval:`audio/mixer` config value to refer
to Mopidy mixer extensions instead of GStreamer mixers. The default value,
``software``, still has the same behavior. All other values will either no
longer work or will at the very least require you to install an additional
extension.
- Changed the :confval:`audio/mixer_volume` config value behavior from
affecting GStreamer mixers to affecting Mopidy mixer extensions instead. The
end result should be the same without any changes to this config value.
- Deprecated the :confval:`audio/mixer_track` config value. This config value
is no longer in use. Mixer extensions that need additional configuration
handle this themselves.
- Use :ref:`proxy-config` when streaming media from the Internet. (Partly
fixing :issue:`390`)
- Fix proper decoding of exception messages that depends on the user's locale.
- Fix recognition of ASX and XSPF playlists with tags in all caps or with
carriage return line endings. (Fixes: :issue:`687`)
- Support simpler ASX playlist variant with ``<ENTRY>`` elements without
children.
- Added ``target_state`` attribute to the audio layer's
:meth:`~mopidy.audio.AudioListener.state_changed` event. Currently, it is
:class:`None` except when we're paused because of buffering. Then the new
field exposes our target state after buffering has completed.
**Mixers**
- Added new :class:`mopidy.mixer.Mixer` API which can be implemented by
extensions.
- Created a bundled extension, :ref:`ext-softwaremixer`, for controlling volume
in software in GStreamer's pipeline. This is Mopidy's default mixer. To use
this mixer, set the :confval:`audio/mixer` config value to ``software``.
- Created an external extension, `Mopidy-ALSAMixer
<https://github.com/mopidy/mopidy-alsamixer/>`_, for controlling volume with
hardware through ALSA. To use this mixer, install the extension, and set the
:confval:`audio/mixer` config value to ``alsamixer``.
**HTTP frontend**
- CherryPy and ws4py have been replaced with Tornado. This will hopefully
reduce CPU usage on OS X (:issue:`445`) and improve error handling in corner
cases, like when returning from suspend (:issue:`718`).
- Added support for packaging web clients as Mopidy extensions and installing
them using pip. See the :ref:`http-server-api` for details. (Fixes:
:issue:`440`)
- Added web page at ``/mopidy/`` which lists all web clients installed as
Mopidy extensions. (Fixes: :issue:`440`)
- Added support for extending the HTTP frontend with additional server side
functionality. See :ref:`http-server-api` for details.
- Exposed the core API using HTTP POST requests with JSON-RPC payloads at
``/mopidy/rpc``. This is the same JSON-RPC interface as is exposed over the
WebSocket at ``/mopidy/ws``, so you can run any core API command.
The HTTP POST interfaces does not give you access to events from Mopidy, like
the WebSocket does. The WebSocket interface is still recommended for web
clients. The HTTP POST interface may be easier to use for simpler programs,
that just needs to query the currently playing track or similar. See
:ref:`http-post-api` for details.
- If Zeroconf is enabled, we now announce the ``_mopidy-http._tcp`` service in
addition to ``_http._tcp``. This is to make it easier to automatically find
Mopidy's HTTP server among other Zeroconf-published HTTP servers on the
local network.
**Mopidy.js client library**
This version has been released to npm as Mopidy.js v0.4.0.
- Update Mopidy.js to use when.js 3. If you maintain a Mopidy client, you
should review the `differences between when.js 2 and 3
<https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x>`_
and the `when.js debugging guide
<https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises>`_.
- All of Mopidy.js' promise rejection values are now of the Error type. This
ensures that all JavaScript VMs will show a useful stack trace if a rejected
promise's value is used to throw an exception. To allow catch clauses to
handle different errors differently, server side errors are of the type
``Mopidy.ServerError``, and connection related errors are of the type
``Mopidy.ConnectionError``.
- Add support for method calls with by-name arguments. The old calling
convention, ``by-position-only``, is still the default, but this will
change in the future. A warning is logged to the console if you don't
explicitly select a calling convention. See the :ref:`mopidy-js` docs for
details.
**MPD frontend**
- Proper command tokenization for MPD requests. This replaces the old regex
based system with an MPD protocol specific tokenizer responsible for breaking
requests into pieces before the handlers have at them.
(Fixes: :issue:`591` and :issue:`592`)
- Updated command handler system. As part of the tokenizer cleanup we've
updated how commands are registered and making it simpler to create new
handlers.
- Simplified a bunch of handlers. All the "browse" type commands now use a
common browse helper under the hood for less repetition. Likewise the query
handling of "search" commands has been somewhat simplified.
- Adds placeholders for missing MPD commands, preparing the way for bumping the
protocol version once they have been added.
- Respond to all pending requests before closing connection. (PR: :issue:`722`)
- Stop incorrectly catching `LookupError` in command handling.
(Fixes: :issue:`741`)
- Browse support for playlists and albums has been added. (PR: :issue:`749`,
:issue:`754`)
- The ``lsinfo`` command now returns browse results before local playlists.
This is helpful as not all clients sort the returned items. (PR:
:issue:`755`)
- Browse now supports different entries with identical names. (PR:
:issue:`762`)
- Search terms that are empty or consists of only whitespace are no longer
included in the search query sent to backends. (PR: :issue:`758`)
**Local backend**
- The JSON local library backend now logs a friendly message telling you about
``mopidy local scan`` if you don't have a local library cache. (Fixes:
:issue:`711`)
- The ``local scan`` command now use multiple threads to walk the file system
and check files' modification time. This speeds up scanning, escpecially
when scanning remote file systems over e.g. NFS.
- the ``local scan`` command now creates necessary folders if they don't
already exist. Previously, this was only done by the Mopidy server, so doing
a ``local scan`` before running the server the first time resulted in a
crash. (Fixes: :issue:`703`)
- Fix proper decoding of exception messages that depends on the user's locale.
**Stream backend**
- Add config value :confval:`stream/metadata_blacklist` to blacklist certain
URIs we should not open to read metadata from before they are opened for
playback. This is typically needed for services that invalidate URIs after a
single use. (Fixes: :issue:`660`)
v0.18.3 (2014-02-16)
====================
@ -649,7 +868,7 @@ one new.
To ease migration we've made a tool named :option:`mopidy-convert-config` for
automatically converting the old ``settings.py`` to a new ``mopidy.conf``
file. This tool takes care of all the renamed config values as well. See
:ref:`mopidy-convert-config` for details on how to use it.
``mopidy-convert-config`` for details on how to use it.
- A long wanted feature: You can now enable or disable specific frontends or
backends without having to redefine :attr:`~mopidy.settings.FRONTENDS` or

View File

@ -15,52 +15,33 @@ created one, please notify us so we can include your client on this page.
See :ref:`http-api` for details on how to build your own web client.
woutervanwijk/Mopidy-Webclient
==============================
Mopidy MusicBox Webclient
=========================
.. image:: woutervanwijk-mopidy-webclient.png
.. image:: mopidy-musicbox-webclient.png
:width: 1275
:height: 600
The first web client for Mopidy, made with jQuery Mobile by Wouter van Wijk.
Also the web client used for Wouter's popular `Pi Musicbox
<http://www.woutervanwijk.nl/pimusicbox/>`_ image for Raspberry Pi.
<http://www.pimusicbox.com/>`_ image for Raspberry Pi.
With Mopidy Browser Client, you can play your music on your computer (or
Rapsberry Pi) and remotely control it from a computer, phone, tablet,
With Mopidy MusicBox Webclient, you can play your music on your computer
(Raspberry Pi) and remotely control it from a computer, phone, tablet,
laptop. From your couch.
-- https://github.com/woutervanwijk/Mopidy-WebClient
This is a responsive HTML/JS/CSS client especially written for Mopidy, a
music server. Responsive, so it works on desktop and mobile browsers. You
can browse, search and play albums, artists, playlists, and it has cover
art from Last.fm.
-- https://github.com/woutervanwijk/Mopidy-MusicBox-Webclient
Mopidy Lux
==========
.. image:: dz0ny-mopidy-lux.png
:width: 1275
:height: 795
A Mopidy web client made with AngularJS by Janez Troha.
A shiny new remote web control interface for Mopidy player.
-- https://github.com/dz0ny/mopidy-lux
.. include:: /ext/lux.rst
Moped
=====
.. image:: martijnboland-moped.png
:width: 720
:height: 450
A Mopidy web client made with Durandal and KnockoutJS by Martijn Boland.
Moped is a responsive web client for the Mopidy music server. It is
inspired by Mopidy-Webclient, but built from scratch based on a different
technology stack with Durandal and Bootstrap 3.
-- https://github.com/martijnboland/moped
.. include:: /ext/moped.rst
JukePi
@ -102,6 +83,23 @@ A Mopidy web client made by Argonaut in SF for their office jukebox.
-- http://blog.argonautinc.com/post/83027259908/music-is-pretty-important-to-our-culture-and
<<<<<<< HEAD
=======
Mopify
======
An in-development web client that clones the Spotify user interface on top of
Mopidy and the Spotify web APIs.
A Mopidy web client based on the Spotify webbased interface. If you use
Mopidy in combination with local music this client probably won't work.
This client uses the Spotify and EchoNest API to speed up searching and
artist/album lookup.
-- https://github.com/dirkgroenen/Mopify
>>>>>>> develop
Other web clients
=================

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -60,6 +60,14 @@ supported)" mode because the client tries to fetch all known metadata and do
the search on the client side. The two other search modes works nicely, so this
is not a problem.
The library view is very slow when used together with Mopidy-Spotify. A
workaround is to edit the ncmpcpp configuration file
(:file:`~/.ncmpcpp/config`) and set::
media_library_display_date = "no"
With this change ncmpcpp's library view will still be a bit slow, but usable.
ncmpc
-----

View File

@ -83,11 +83,11 @@ 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
.. describe:: local clear
Clear local media files from the local library.
.. cmdoption:: local scan
.. describe:: local scan
Scan local media files present in your library.
@ -130,12 +130,6 @@ The ``mopidy config`` output shows the effect of the :option:`--option` flags::
mopidy -o mpd/enabled=false -o spotify/bitrate=320 config
See also
========
:ref:`mopidy-convert-config(1) <mopidy-convert-config>`
Reporting bugs
==============

View File

@ -1,13 +0,0 @@
.. _commands:
********
Commands
********
Mopidy comes with the following commands:
.. toctree::
:maxdepth: 1
:glob:
**

View File

@ -1,98 +0,0 @@
.. _mopidy-convert-config:
*****************************
mopidy-convert-config command
*****************************
Synopsis
========
mopidy-convert-config
Description
===========
Mopidy is a music server which can play music both from multiple sources, like
your local hard drive, radio streams, and from Spotify and SoundCloud. Searches
combines results from all music sources, and you can mix tracks from all
sources in your play queue. Your playlists from Spotify or SoundCloud are also
available for use.
The ``mopidy-convert-config`` command is used to convert :file:`settings.py`
configuration files used by ``mopidy`` < 0.14 to the :file:`mopidy.conf` config
file used by ``mopidy`` >= 0.14.
Options
=======
.. program:: mopidy-convert-config
This program does not take any options. It looks for the pre-0.14 settings file
at :file:`{$XDG_CONFIG_DIR}/mopidy/settings.py`, and if it exists it converts
it and ouputs a Mopidy 0.14 compatible ini-format configuration. If you don't
already have a config file at :file:`{$XDG_CONFIG_DIR}/mopidy/mopidy.conf``,
you're asked if you want to save the converted config to that file.
Example
=======
Given the following contents in :file:`~/.config/mopidy/settings.py`:
::
LOCAL_MUSIC_PATH = u'~/music'
MPD_SERVER_HOSTNAME = u'::'
SPOTIFY_PASSWORD = u'secret'
SPOTIFY_USERNAME = u'alice'
Running ``mopidy-convert-config`` will convert the config and create a new
:file:`mopidy.conf` config file:
.. code-block:: none
$ mopidy-convert-config
Checking /home/alice/.config/mopidy/settings.py
Converted config:
[spotify]
username = alice
password = ********
[mpd]
hostname = ::
[local]
media_dir = ~/music
Write new config to /home/alice/.config/mopidy/mopidy.conf? [yN] y
Done.
Contents of :file:`~/.config/mopidy/mopidy.conf` after the conversion:
.. code-block:: ini
[spotify]
username = alice
password = secret
[mpd]
hostname = ::
[local]
media_dir = ~/music
See also
========
:ref:`mopidy(1) <mopidy-cmd>`
Reporting bugs
==============
Report bugs to Mopidy's issue tracker at
<https://github.com/mopidy/mopidy/issues>

View File

@ -35,8 +35,6 @@ class Mock(object):
# glib.get_user_config_dir()
return str
elif (name[0] == name[0].upper()
# gst.interfaces.MIXER_TRACK_*
and not name.startswith('MIXER_TRACK_')
# gst.PadTemplate
and not name.startswith('PadTemplate')
# dbus.String()
@ -89,6 +87,7 @@ extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.extlinks',
'sphinx.ext.graphviz',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
]
@ -141,19 +140,12 @@ latex_documents = [
man_pages = [
(
'commands/mopidy',
'command',
'mopidy',
'music server',
'',
'1'
),
(
'commands/mopidy-convert-config',
'mopidy-convert-config',
'migrate config files from mopidy pre-0.14',
'',
'1'
),
]
@ -165,3 +157,12 @@ extlinks = {
'mpris': (
'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'),
}
# -- Options for intersphinx extension ----------------------------------------
intersphinx_mapping = {
'python': ('http://docs.python.org/2', None),
'pykka': ('http://www.pykka.org/en/latest/', None),
'tornado': ('http://www.tornadoweb.org/en/stable/', None),
}

View File

@ -45,14 +45,6 @@ below, together with their default values. In addition, all :ref:`extensions
defaults are documented on the :ref:`extension pages <ext>`.
Migrating from pre 0.14
=======================
For those users upgrading from versions prior to 0.14 we made
the :option:`mopidy-convert-config` tool, to ease the process of migrating
settings to the new config format.
Default core configuration
==========================
@ -72,23 +64,15 @@ Audio configuration
Audio mixer to use.
Expects a GStreamer mixer to use, typical values are: ``software``,
``autoaudiomixer``, ``alsamixer``, ``pulsemixer``, ``ossmixer``, and
``oss4mixer``.
The default is ``software``, which does volume control inside Mopidy before
the audio is sent to the audio output. This mixer does not affect the
volume of any other audio playback on the system. It is the only mixer that
will affect the audio volume if you're streaming the audio from Mopidy
through Shoutcast.
If you want to use a hardware mixer, try ``autoaudiomixer``. It attempts to
select a sane hardware mixer for you automatically. When Mopidy is started,
it will log what mixer ``autoaudiomixer`` selected, for example::
INFO Audio mixer set to "alsamixer" using track "Master"
Setting the config value to blank turns off volume control.
If you want to use a hardware mixer, you need to install a Mopidy extension
which integrates with your sound subsystem. E.g. for ALSA, install
`Mopidy-ALSAMixer <https://github.com/mopidy/mopidy-alsamixer>`_.
.. confval:: audio/mixer_volume
@ -99,14 +83,6 @@ Audio configuration
Setting the config value to blank leaves the audio mixer volume unchanged.
For the software mixer blank means 100.
.. confval:: audio/mixer_track
Audio mixer track to use.
Name of the mixer track to use. If this is not set we will try to find the
master output track. As an example, using ``alsamixer`` you would typically
set this to ``Master`` or ``PCM``.
.. confval:: audio/output
Audio output to use.
@ -131,6 +107,11 @@ Audio configuration
Logging configuration
---------------------
.. confval:: logging/color
Whether or not to colorize the console log based on log level. Defaults to
``true``.
.. confval:: logging/console_format
The log format used for informational logging.
@ -164,6 +145,8 @@ Logging configuration
.. _the Python logging docs: http://docs.python.org/2/library/logging.config.html
.. _proxy-config:
Proxy configuration
-------------------
@ -278,13 +261,21 @@ server simultaneously. To use the SHOUTcast output, do the following:
#. You might also need to change the ``shout2send`` default settings, run
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
you want to change ``ip``, ``username``, ``password``, and ``mount``. For
example:
you want to change ``ip``, ``username``, ``password``, and ``mount``.
Example for MP3 streaming:
.. code-block:: ini
[audio]
output = lame ! shout2send username="alice" password="secret" mount="mopidy"
output = lame ! shout2send mount=mopidy ip=127.0.0.1 port=8000 password=hackme
Example for Ogg Vorbis streaming:
.. code-block:: ini
[audio]
output = audioresample ! audioconvert ! vorbisenc ! oggmux ! shout2send mount=mopidy ip=127.0.0.1 port=8000 password=hackme
Other advanced setups are also possible for outputs. Basically, anything you
can use with the ``gst-launch-0.10`` command can be plugged into

View File

@ -35,6 +35,10 @@ Making changes
#. Install dependencies as described in the :ref:`installation` section.
#. Install additional development dependencies::
pip install -r dev-requirements.txt
#. Checkout a new branch (usually based on ``develop``) and name it accordingly
to what you intend to do.
@ -82,29 +86,33 @@ Testing
Mopidy has quite good test coverage, and we would like all new code going into
Mopidy to come with tests.
#. To run tests, you need a couple of dependencies. They can be installed using
``pip``::
pip install --upgrade coverage flake8 mock nose
#. Then, to run all tests, go to the project directory and run::
#. To run all tests, go to the project directory and run::
nosetests
To run tests with test coverage statistics, remember to specify the tests
dir::
To run tests with test coverage statistics::
nosetests --with-coverage tests/
nosetests --with-coverage
Test coverage statistics can also be viewed online at
`coveralls.io <https://coveralls.io/r/mopidy/mopidy>`_.
#. Check the code for errors and style issues using flake8::
#. Always check the code for errors and style issues using flake8::
flake8 .
flake8
For more documentation on testing, check out the `nose documentation
<http://nose.readthedocs.org/>`_.
If successful, the command will not print anything at all.
#. Finally, there is the ultimate but a bit slower command. To run both tests,
docs build, and flake8 linting, run::
tox
This will run exactly the same tests as `Travis CI
<https://travis-ci.org/mopidy/mopidy>`_ runs for all our branches and pull
requests. If this command turns green, you can be quite confident that your
pull request will get the green flag from Travis as well, which is a
requirement for it to be merged.
Submitting changes

View File

@ -12,7 +12,7 @@ Debian-based Linux distributions.
Installation
============
See the Debian/Ubuntu section in the :ref:`installation` section.
See :ref:`debian-install`.
Running as a system service

View File

@ -54,7 +54,7 @@ Creating releases
#. Update changelog and commit it.
#. Bump the version number in ``mopidy/__init__.py``. Remember to update the
test case in ``tests/version_test.py``.
test case in ``tests/test_version.py``.
#. Merge the release branch (``develop`` in the example) into master::

BIN
docs/ext/api_explorer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

12
docs/ext/api_explorer.rst Normal file
View File

@ -0,0 +1,12 @@
.. _http-explore-extension:
Mopidy-API-Explorer
===================
https://github.com/dz0ny/mopidy-api-explorer
Web extension for browsing the Mopidy HTTP API.
.. image:: /ext/api_explorer.png
:width: 1176
:height: 713

View File

@ -1,27 +1,14 @@
*******************
External extensions
*******************
.. _ext-backends:
******************
Backend extensions
******************
Here you can find a list of external packages that extend Mopidy with
additional functionality. This list is moderated and updated on a regular
basis. If you want your package to show up here, follow the :ref:`guide on
creating extensions <extensiondev>`.
additional music sources by implementing the :ref:`backend-api`.
Mopidy also bundles some extensions:
- :ref:`ext-local`
- :ref:`ext-stream`
- :ref:`ext-http`
- :ref:`ext-mpd`
Mopidy-Arcam
============
https://github.com/TooDizzy/mopidy-arcam
Extension for controlling volume using an external Arcam amplifier. Developed
and tested with an Arcam AVR-300.
This list is moderated and updated on a regular basis. If you want your package
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
Mopidy-Beets
@ -60,29 +47,10 @@ Extension for playing music and audio from the `Internet Archive
<https://archive.org/>`_.
Mopidy-MPRIS
Mopidy-Local
============
https://github.com/mopidy/mopidy-mpris
Extension for controlling Mopidy through the `MPRIS <http://www.mpris.org/>`_
D-Bus interface, for example using the Ubuntu Sound Menu.
Mopidy-NAD
==========
https://github.com/mopidy/mopidy-nad
Extension for controlling volume using an external NAD amplifier.
Mopidy-Notifier
===============
https://github.com/sauberfred/mopidy-notifier
Extension for displaying track info as User Notifications in Mac OS X.
Bundled with Mopidy. See :ref:`ext-local`.
Mopidy-Podcast
@ -111,6 +79,24 @@ Extension for Mopidy-Podcast that lets you search and browse podcasts from the
Apple iTunes Store.
Mopidy-Podcast-gpodder.net
==========================
https://github.com/tkem/mopidy-podcast-gpodder
Extension for Mopidy-Podcast that lets you search and browse podcasts from the
`gpodder.net <https://gpodder.net/>`_ web site.
Mopidy-Podcast-iTunes
=====================
https://github.com/tkem/mopidy-podcast-itunes
Extension for Mopidy-Podcast that lets you search and browse podcasts from the
Apple iTunes Store.
Mopidy-radio-de
===============
@ -121,14 +107,6 @@ Extension for listening to Internet radio stations and podcasts listed at
`radio.fr <http://www.radio.fr/>`_, and `radio.at <http://www.radio.at/>`_.
Mopidy-Scrobbler
================
https://github.com/mopidy/mopidy-scrobbler
Extension for scrobbling played tracks to Last.fm.
Mopidy-SomaFM
=============
@ -156,6 +134,22 @@ Extension for playing music from the `Spotify <http://www.spotify.com/>`_ music
streaming service.
Mopidy-Spotify-Tunigo
=====================
https://github.com/trygveaa/mopidy-spotify-tunigo
Extension for providing the browse feature of `Spotify
<http://www.spotify.com/>`_. This lets you browse playlists, genres and new
releases.
Mopidy-Stream
=============
Bundled with Mopidy. See :ref:`ext-stream`.
Mopidy-Subsonic
===============
@ -183,17 +177,8 @@ Provides a backend for playing music from the `VKontakte social network
<http://vk.com/>`_.
Mopidy-Yamaha
=============
https://github.com/knutz3n/mopidy-yamaha
Extension for controlling volume using an external Yamaha network connected
amplifier.
Mopidy-YouTube
=================
==============
https://github.com/dz0ny/mopidy-youtube

49
docs/ext/frontends.rst Normal file
View File

@ -0,0 +1,49 @@
.. _ext-frontends:
*******************
Frontend extensions
*******************
Here you can find a list of external packages that extend Mopidy with
additional frontends, which includes just about anything that use the
:ref:`core-api`.
This list is moderated and updated on a regular basis. If you want your package
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
Mopidy-HTTP
===========
Bundled with Mopidy. See :ref:`ext-http`.
Mopidy-MPD
==========
Bundled with Mopidy. See :ref:`ext-mpd`.
Mopidy-MPRIS
============
https://github.com/mopidy/mopidy-mpris
Extension for controlling Mopidy through the `MPRIS <http://www.mpris.org/>`_
D-Bus interface, for example using the Ubuntu Sound Menu.
Mopidy-Notifier
===============
https://github.com/sauberfred/mopidy-notifier
Extension for displaying track info as User Notifications in Mac OS X.
Mopidy-Scrobbler
================
https://github.com/mopidy/mopidy-scrobbler
Extension for scrobbling played tracks to Last.fm.

View File

@ -6,7 +6,7 @@ Mopidy-HTTP
Mopidy-HTTP is an extension that lets you control Mopidy through HTTP and
WebSockets, for example from a web client. It is bundled with Mopidy and
enabled by default if all dependencies are available.
enabled by default.
When it is enabled it starts a web server at the port specified by the
:confval:`http/port` config value.
@ -47,24 +47,24 @@ you're looking for a web based client for Mopidy, go check out
:ref:`http-clients`.
Dependencies
============
Extending the server's functionality
====================================
In addition to Mopidy's dependencies, Mopidy-HTTP requires the following:
If you wish to extend the server with additional server side functionality you
must create class that implements the :class:`mopidy.http.Router` interface and
install it in the extension registry under the ``http:router`` name.
- cherrypy >= 3.2.2. Available as python-cherrypy3 in Debian/Ubuntu.
The default implementation of :class:`mopidy.http.Router` already supports
serving static files. If you just want to serve static files you only need to
define the class variables :attr:`mopidy.http.Router.name` and
:attr:`mopidy.http.Router.path`. For example::
- ws4py >= 0.2.3. Available as python-ws4py in newer Debian/Ubuntu and from
`apt.mopidy.com <http://apt.mopidy.com/>`__ for older releases of
Debian/Ubuntu.
class MyWebClient(http.Router):
name = 'mywebclient'
path = os.path.join(os.path.dirname(__file__), 'public_html')
If you're installing Mopidy with pip, you can run the following command to
install Mopidy with the extra dependencies for required for Mopidy-HTTP::
pip install --upgrade Mopidy[http]
If you're installing Mopidy from APT, the additional dependencies needed for
Mopidy-HTTP are always included.
If you wish to extend server with custom methods you can override the method
:meth:`mopidy.http.Router.setup_routes` and define custom routes.
Configuration
@ -108,4 +108,7 @@ See :ref:`config` for general help on configuring Mopidy.
Name of the HTTP service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
If set, the Zeroconf services ``_http._tcp`` and ``_mopidy-http._tcp`` will
be published.
Set to an empty string to disable Zeroconf for HTTP.

View File

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 351 KiB

10
docs/ext/lux.rst Normal file
View File

@ -0,0 +1,10 @@
Mopidy-Lux
==========
https://github.com/dz0ny/mopidy-lux
A Mopidy web client made with AngularJS by Janez Troha.
.. image:: /ext/lux.png
:width: 1275
:height: 795

53
docs/ext/mixers.rst Normal file
View File

@ -0,0 +1,53 @@
.. _ext-mixers:
****************
Mixer extensions
****************
Here you can find a list of external packages that extend Mopidy with
additional audio mixers by implementing the :ref:`mixer-api` which was added
in Mopidy 0.19.
This list is moderated and updated on a regular basis. If you want your package
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
Mopidy-ALSAMixer
================
https://github.com/mopidy/mopidy-alsamixer
Extension for controlling volume one a Linux system using ALSA.
Mopidy-Arcam
============
https://github.com/TooDizzy/mopidy-arcam
Extension for controlling volume using an external Arcam amplifier. Developed
and tested with an Arcam AVR-300.
Mopidy-NAD
==========
https://github.com/mopidy/mopidy-nad
Extension for controlling volume using an external NAD amplifier. Developed
and tested with a NAD C355BEE.
Mopidy-SoftwareMixer
====================
Bundled with Mopidy. See :ref:`ext-softwaremixer`.
Mopidy-Yamaha
=============
https://github.com/knutz3n/mopidy-yamaha
Extension for controlling volume using an external Yamaha network connected
amplifier.

View File

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

10
docs/ext/moped.rst Normal file
View File

@ -0,0 +1,10 @@
Moped
=====
https://github.com/martijnboland/moped
A Mopidy web client made with AnbularJS by Martijn Boland.
.. image:: /ext/moped.png
:width: 720
:height: 450

View File

@ -0,0 +1,35 @@
.. _ext-softwaremixer:
********************
Mopidy-SoftwareMixer
********************
Mopidy-SoftwareMixer is an extension for controlling audio volume in software
through GStreamer. It is the only mixer bundled with Mopidy and is enabled by
default.
If you use PulseAudio, the software mixer will control the per-application
volume for Mopidy in PulseAudio, and any changes to the per-application volume
done from outside Mopidy will be reflected by the software mixer.
If you don't use PulseAudio, the mixer will adjust the volume internally in
Mopidy's GStreamer pipeline.
Configuration
=============
Multiple mixers can be installed and enabled at the same time, but only the
mixer pointed to by the :confval:`audio/mixer` config value will actually be
used.
See :ref:`config` for general help on configuring Mopidy.
.. literalinclude:: ../../mopidy/softwaremixer/ext.conf
:language: ini
.. confval:: softwaremixer/enabled
If the software mixer should be enabled or not. Usually you don't want to
change this, but instead change the :confval:`audio/mixer` config value to
decide which mixer is actually used.

View File

@ -42,3 +42,10 @@ See :ref:`config` for general help on configuring Mopidy.
.. confval:: stream/timeout
Number of milliseconds before giving up looking up stream metadata.
.. confval:: stream/metadata_blacklist
List of URI globs to not fetch metadata from before playing. This feature
is typically needed for play once URIs provided by certain streaming
providers. Regular POSIX glob semantics apply, so ``http://*.example.com/*``
would match all example.com sub-domains.

21
docs/ext/web.rst Normal file
View File

@ -0,0 +1,21 @@
.. _ext-web:
**************
Web extensions
**************
Here you can find a list of external packages that extend Mopidy with
additional web interfaces by implementing the :ref:`http-server-api`, which
was added in Mopidy 0.19, and optionally using the :ref:`http-api`.
This list is moderated and updated on a regular basis. If you want your package
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
.. include:: /ext/api_explorer.rst
.. include:: /ext/lux.rst
.. include:: /ext/moped.rst

View File

@ -285,7 +285,7 @@ This is ``mopidy_soundspot/__init__.py``::
version = __version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__, 'ext.conf'))
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
@ -413,6 +413,15 @@ more details.
return 0
Example web application
=======================
As of Mopidy 0.19, extensions can use Mopidy's builtin web server to host
static web clients as well as Tornado and WSGI web applications. For several
examples, see the :ref:`http-server-api` docs or explore with
:ref:`http-explore-extension` extension.
Example GStreamer element
=========================
@ -423,9 +432,6 @@ Basically, you just implement your GStreamer element in Python and then make
your :meth:`~mopidy.ext.Extension.setup` method register all your custom
GStreamer elements.
For examples of custom GStreamer elements implemented in Python, see
:mod:`mopidy.audio.mixers`.
Python conventions
==================

View File

@ -33,7 +33,6 @@ Usage
:maxdepth: 2
installation/index
installation/raspberrypi
config
running
troubleshooting
@ -52,7 +51,11 @@ Extensions
ext/stream
ext/http
ext/mpd
ext/external
ext/softwaremixer
ext/mixers
ext/backends
ext/frontends
ext/web
Clients
@ -97,7 +100,7 @@ Reference
:maxdepth: 2
glossary
commands/index
command
api/index
modules/index

View File

@ -0,0 +1,27 @@
.. _arch-install:
****************************
Arch Linux: Install from AUR
****************************
If you are running Arch Linux, you can install Mopidy using the
`mopidy <https://aur.archlinux.org/packages/mopidy/>`_ package found in AUR.
#. To install Mopidy with all dependencies, you can use
for example `yaourt <https://wiki.archlinux.org/index.php/yaourt>`_::
yaourt -S mopidy
To upgrade Mopidy to future releases, just upgrade your system using::
yaourt -Syu
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
Last.fm scrobbling, AUR also has `packages for several Mopidy extensions
<https://aur.archlinux.org/packages/?K=mopidy>`_.
For a full list of available Mopidy extensions, including those not
installable from AUR, see :ref:`ext`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.

View File

@ -0,0 +1,64 @@
.. _debian-install:
******************************************
Debian/Ubuntu: Install from apt.mopidy.com
******************************************
If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
install Mopidy is from the `Mopidy APT archive <https://apt.mopidy.com/>`_.
When installing from the APT archive, you will automatically get updates to
Mopidy in the same way as you get updates to the rest of your system.
If you're on a Raspberry Pi running Debian or Raspbian, the following
instructions should work for you as well. If you're setting up a Raspberry Pi
from scratch, we have a guide for installing Debian/Raspbian and Mopidy. See
:ref:`raspberrypi-installation`.
#. Add the archive's GPG key::
wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
#. Add the following to ``/etc/apt/sources.list``, or if you have the directory
``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in
that directory::
# Mopidy APT archive
deb http://apt.mopidy.com/ stable main contrib non-free
deb-src http://apt.mopidy.com/ stable main contrib non-free
For the lazy, you can simply run the following command to create
``/etc/apt/sources.list.d/mopidy.list``::
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/mopidy.list
#. Install Mopidy and all dependencies::
sudo apt-get update
sudo apt-get install mopidy
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
Last.fm scrobbling, you need to install additional 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`.
#. Before continuing, make sure you've read the :ref:`debian` section to learn
about the differences between running Mopidy as a system service and
manually as your own system user.
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
you're ready to :doc:`run Mopidy </running>`.
When a new release of Mopidy is out, and you can't wait for you system to
figure it out for itself, run the following to upgrade right away::
sudo apt-get update
sudo apt-get dist-upgrade

View File

@ -5,295 +5,15 @@ Installation
************
There are several ways to install Mopidy. What way is best depends upon your OS
and/or distribution. If you want to contribute to the development of Mopidy,
you should first read this page, then have a look at :ref:`run-from-git`.
and/or distribution.
.. contents:: Installation guides
:local:
If you want to contribute to the development of Mopidy, you should first read
the general installation instructions, then have a look at :ref:`run-from-git`.
.. toctree::
Debian/Ubuntu: Install from apt.mopidy.com
==========================================
If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
install Mopidy is from the `Mopidy APT archive <http://apt.mopidy.com/>`_. When
installing from the APT archive, you will automatically get updates to Mopidy
in the same way as you get updates to the rest of your distribution.
#. Add the archive's GPG key::
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
#. Add the following to ``/etc/apt/sources.list``, or if you have the directory
``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in
that directory::
# Mopidy APT archive
deb http://apt.mopidy.com/ stable main contrib non-free
deb-src http://apt.mopidy.com/ stable main contrib non-free
For the lazy, you can simply run the following command to create
``/etc/apt/sources.list.d/mopidy.list``::
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
#. Install Mopidy and all dependencies::
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`.
#. Before continuing, make sure you've read the :ref:`debian` section to learn
about the differences between running Mopidy as a system service and
manually as your own system user.
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
you're ready to :doc:`run Mopidy </running>`.
When a new release of Mopidy is out, and you can't wait for you system to
figure it out for itself, run the following to upgrade right away::
sudo apt-get update
sudo apt-get dist-upgrade
Raspberry Pi running Debian
---------------------------
We have a guide for installing a Raspberry Pi from scratch with Debian/Raspbian
and Mopidy. See :ref:`raspberrypi-installation`.
Arch Linux: Install from AUR
============================
If you are running Arch Linux, you can install Mopidy
using the `mopidy <https://aur.archlinux.org/packages/mopidy/>`_
package found in AUR.
#. To install Mopidy with all dependencies, you can use
for example `yaourt <https://wiki.archlinux.org/index.php/yaourt>`_::
yaourt -S mopidy
To upgrade Mopidy to future releases, just upgrade your system using::
yaourt -Syu
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
Last.fm scrobbling, AUR also has `packages for several Mopidy extensions
<https://aur.archlinux.org/packages/?K=mopidy>`_.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
OS X: Install from Homebrew and pip
===================================
If you are running OS X, you can install everything needed with Homebrew and
pip.
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
If you are already using Homebrew, make sure your installation is up to
date before you continue::
brew update
brew upgrade
#. Mopidy requires GStreamer 0.10, but Homebrew's main formula repo has
upgraded its GStreamer packages to 1.0. Thus, you'll need to add an
alternative formula repo (aka "tap") that has the old GStreamer formulas::
brew tap homebrew/versions
#. Install the required packages from Homebrew::
brew install gst-python010 gst-plugins-good010 gst-plugins-ugly010
#. Make sure to include Homebrew's Python ``site-packages`` directory in your
``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
and it will crash.
You can either amend your ``PYTHONPATH`` permanently, by adding the
following statement to your shell's init file, e.g. ``~/.bashrc``::
export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
Or, you can prefix the Mopidy command every time you run it::
PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
#. Next up, you need to install some Python packages. To do so, we use pip. If
you don't have the ``pip`` command, you can install it now::
sudo easy_install pip
#. Then, install the latest release of Mopidy using pip::
sudo pip install -U mopidy
#. Optionally, install additional extensions to Mopidy.
For HTTP frontend support, so you can run Mopidy web clients::
sudo pip install -U mopidy[http]
For playing music from Spotify::
brew install libspotify
sudo pip install -U mopidy-spotify
For scrobbling to Last.fm::
sudo pip install -U mopidy-scrobbler
For more extensions, see :ref:`ext`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
Otherwise: Install from source using pip
========================================
If you are on on Linux, but can't install from the APT archive or from AUR, you
can install Mopidy from PyPI using pip.
#. First of all, you need Python 2.7. Check if you have Python and what
version by running::
python --version
#. When you install using pip, you need to make sure you have pip. You'll also
need a C compiler and the Python development headers to build pyspotify
later.
This is how you install it on Debian/Ubuntu::
sudo apt-get install build-essential python-dev python-pip
And on Arch Linux from the official repository::
sudo pacman -S base-devel python2-pip
And on Fedora Linux from the official repositories::
sudo yum install -y gcc python-devel python-pip
.. note::
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
following steps.
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python
bindings. GStreamer is packaged for most popular Linux distributions. Search
for GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets.
If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools
If you use Arch Linux, install the following packages from the official
repository::
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
gstreamer0.10-ugly-plugins
If you use Fedora you can install GStreamer like this::
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools
If you use Gentoo you need to be careful because GStreamer 0.10 is in a
different lower slot than 1.0, the default. Your emerge commands will need
to include the slot::
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc.
#. Install the latest release of Mopidy::
sudo pip install -U mopidy
To upgrade Mopidy to future releases, just rerun this command.
Alternatively, if you want to track Mopidy development closer, you may
install a snapshot of Mopidy's ``develop`` Git branch using pip::
sudo pip install --allow-unverified=mopidy mopidy==dev
#. Optional: If you want to use the HTTP frontend and web clients, you need
some additional dependencies::
sudo pip install -U mopidy[http]
#. Optional: If you want Spotify support in Mopidy, you'll need to install
libspotify and the Mopidy-Spotify extension.
#. Download and install the latest version of libspotify for your OS and CPU
architecture from `Spotify
<https://developer.spotify.com/technologies/libspotify/>`_.
For libspotify 12.1.51 for 64-bit Linux the process is as follows::
wget https://developer.spotify.com/download/libspotify/libspotify-12.1.51-Linux-x86_64-release.tar.gz
tar zxfv libspotify-12.1.51-Linux-x86_64-release.tar.gz
cd libspotify-12.1.51-Linux-x86_64-release/
sudo make install prefix=/usr/local
Remember to adjust the above example for the latest libspotify version
supported by pyspotify, your OS, and your CPU architecture.
#. If you're on Fedora, you must add a configuration file so libspotify.so
can be found::
echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/libspotify.conf
sudo ldconfig
#. Then install the latest release of Mopidy-Spotify using pip::
sudo pip install -U mopidy-spotify
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
to install Mopidy-Scrobbler::
sudo pip install -U mopidy-scrobbler
#. Optional: To use Mopidy-MPRIS, e.g. for controlling Mopidy from the Ubuntu
Sound Menu or from an UPnP client via Rygel, you need some additional
dependencies and the Mopidy-MPRIS extension.
#. Install the Python bindings for libindicate, and the Python bindings for
libdbus, the reference D-Bus library.
On Debian/Ubuntu::
sudo apt-get install python-dbus python-indicate
#. Then install the latest release of Mopidy-MPRIS using pip::
sudo pip install -U mopidy-mpris
#. For more Mopidy extensions, see :ref:`ext`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
debian
arch
osx
source
raspberrypi

65
docs/installation/osx.rst Normal file
View File

@ -0,0 +1,65 @@
***************************
OS X: Install from Homebrew
***************************
If you are running OS X, you can install everything needed with Homebrew.
#. Install Xcode command line developer tools. Do this even if you already have
Xcode installed::
xcode-select --install
#. Install `XQuartz <http://xquartz.macosforge.org/>`_. This is needed by
GStreamer which Mopidy use heavily.
#. Install `Homebrew <https://github.com/Homebrew/homebrew>`_.
#. If you are already using Homebrew, make sure your installation is up to
date before you continue::
brew update
brew upgrade
#. Mopidy works out of box if you have installed Python from Homebrew::
brew install python
.. note::
If you want to use the Python version bundled with OS X, you'll need to
include Python packages installed by Homebrew in your ``PYTHONPATH``.
If you don't do this, the ``mopidy`` executable will not find its
dependencies and will crash.
You can either amend your ``PYTHONPATH`` permanently, by adding the
following statement to your shell's init file, e.g. ``~/.bashrc``::
export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
Or, you can prefix the Mopidy command every time you run it::
PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
#. Mopidy has its own `Homebrew formula repo
<https://github.com/mopidy/homebrew-mopidy>`_, called a "tap". To enable our
Homebrew tap, run::
brew tap mopidy/mopidy
#. To install Mopidy, run::
brew install mopidy
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
Last.fm scrobbling, the Homebrew tap has formulas for several Mopidy
extensions as well.
To list all the extensions available from our tap, you can run::
brew search mopidy
For a full list of available Mopidy extensions, including those not
installable from Homebrew, see :ref:`ext`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.

View File

@ -1,8 +1,8 @@
.. _raspberrypi-installation:
****************************
Installation on Raspberry Pi
****************************
*************************************
Raspberry Pi: Mopidy on a credit card
*************************************
Mopidy runs nicely on a `Raspberry Pi <http://www.raspberrypi.org/>`_. As of
January 2013, Mopidy will run with Spotify support on both the armel
@ -71,11 +71,12 @@ you a lot better performance.
command to e.g. ``/etc/rc.local``, which will be executed when the system is
booting.
#. Install Mopidy and its dependencies from `apt.mopidy.com
<http://apt.mopidy.com/>`_, as described in :ref:`installation`.
#. Install Mopidy and its dependencies as described in :ref:`debian-install`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
then you're ready to :doc:`run Mopidy </running>`. Alternatively you may
want to have Mopidy run as a :doc:`system service </debian>`, automatically
starting at boot.
Appendix: Fixing audio quality issues

View File

@ -0,0 +1,114 @@
.. _source-install:
*******************
Install from source
*******************
If you are on Linux, but can't install :ref:`from the APT archive
<debian-install>` or :ref:`from AUR <arch-install>`, you can install Mopidy
from source by hand.
#. First of all, you need Python 2.7. Check if you have Python and what
version by running::
python --version
#. You need to make sure you have ``pip``, the Python package installer. You'll
also need a C compiler and the Python development headers to build pyspotify
later.
This is how you install it on Debian/Ubuntu::
sudo apt-get install build-essential python-dev python-pip
And on Arch Linux from the official repository::
sudo pacman -S base-devel python2-pip
And on Fedora Linux from the official repositories::
sudo yum install -y gcc python-devel python-pip
.. note::
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
following steps.
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python
bindings. GStreamer is packaged for most popular Linux distributions. Search
for GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets.
If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools
If you use Arch Linux, install the following packages from the official
repository::
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
gstreamer0.10-ugly-plugins
If you use Fedora you can install GStreamer like this::
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools
If you use Gentoo you need to be careful because GStreamer 0.10 is in a
different lower slot than 1.0, the default. Your emerge commands will need
to include the slot::
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc.
#. Install the latest release of Mopidy::
sudo pip install -U mopidy
To upgrade Mopidy to future releases, just rerun this command.
Alternatively, if you want to track Mopidy development closer, you may
install a snapshot of Mopidy's ``develop`` Git branch using pip::
sudo pip install --allow-unverified=mopidy mopidy==dev
#. Optional: If you want Spotify support in Mopidy, you'll need to install
libspotify and the Mopidy-Spotify extension.
#. Download and install the latest version of libspotify for your OS and CPU
architecture from `Spotify
<https://developer.spotify.com/technologies/libspotify/>`_.
For libspotify 12.1.51 for 64-bit Linux the process is as follows::
wget https://developer.spotify.com/download/libspotify/libspotify-12.1.51-Linux-x86_64-release.tar.gz
tar zxfv libspotify-12.1.51-Linux-x86_64-release.tar.gz
cd libspotify-12.1.51-Linux-x86_64-release/
sudo make install prefix=/usr/local
Remember to adjust the above example for the latest libspotify version
supported by pyspotify, your OS, and your CPU architecture.
#. If you're on Fedora, you must add a configuration file so libspotify.so
can be found::
echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/libspotify.conf
sudo ldconfig
#. Then install the latest release of Mopidy-Spotify using pip::
sudo pip install -U mopidy-spotify
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
to install Mopidy-Scrobbler::
sudo pip install -U mopidy-scrobbler
#. For a full list of available Mopidy extensions, see :ref:`ext`.
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.

View File

@ -7,6 +7,13 @@ For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
.. automodule:: mopidy.mpd
:synopsis: MPD server frontend
MPD tokenizer
=============
.. automodule:: mopidy.mpd.tokenize
:synopsis: MPD request tokenizer
:members:
MPD dispatcher
==============

View File

@ -7,7 +7,7 @@ Troubleshooting
If you run into problems with Mopidy, we usually hang around at ``#mopidy`` at
`irc.freenode.net <http://freenode.net/>`_ and also have a `mailing list at
Google Groups <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_.
If you stumble into a bug or got a feature request, please create an issue in
If you stumble into a bug or have a feature request, please create an issue in
the `issue tracker <https://github.com/mopidy/mopidy/issues>`_.
When you're debugging yourself or asking for help, there are some tools built
@ -64,7 +64,7 @@ docs for the :confval:`loglevels/*` config section.
Debugging deadlocks
===================
If Mopidy hangs without and obvious explanation, you can send the ``SIGUSR1``
If Mopidy hangs without an obvious explanation, you can send the ``SIGUSR1``
signal to the Mopidy process. If Mopidy's main thread is still responsive, it
will log a traceback for each running thread, showing what the threads are
currently doing. This is a very useful tool for understanding exactly how the

View File

@ -2,8 +2,9 @@
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
meta: {
banner: "/*! Mopidy.js - built " +
banner: "/*! Mopidy.js v<%= pkg.version %> - built " +
"<%= grunt.template.today('yyyy-mm-dd') %>\n" +
" * http://www.mopidy.com/\n" +
" * Copyright (c) <%= grunt.template.today('yyyy') %> " +
@ -26,7 +27,7 @@ module.exports = function (grunt) {
},
options: {
postBundleCB: function (err, src, next) {
next(null, grunt.template.process("<%= meta.banner %>") + src);
next(err, grunt.template.process("<%= meta.banner %>") + src);
},
standalone: "Mopidy"
}
@ -45,7 +46,7 @@ module.exports = function (grunt) {
},
options: {
postBundleCB: function (err, src, next) {
next(null, grunt.template.process("<%= meta.banner %>") + src);
next(err, grunt.template.process("<%= meta.banner %>") + src);
},
standalone: "Mopidy"
}

View File

@ -41,20 +41,15 @@ After npm completes, you can import Mopidy.js using ``require()``:
Using the library
-----------------
See Mopidy's [HTTP API
documentation](http://docs.mopidy.com/en/latest/api/http/).
See the [Mopidy.js documentation](http://docs.mopidy.com/en/latest/api/js/).
Building from source
--------------------
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're
running Ubuntu:
1. Install [Node.js](http://nodejs.org/) and npm. If you're running Ubuntu:
sudo apt-get install python-software-properties
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install nodejs-legacy npm
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
@ -85,6 +80,27 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in
Changelog
---------
### 0.4.0 (2014-06-24)
- Add support for method calls with by-name arguments. The old calling
convention, "by-position-only", is still the default, but this will change in
the future. A warning is printed to the console if you don't explicitly
select a calling convention. See the docs for details.
### 0.3.0 (2014-06-16)
- Upgrade to when.js 3, which brings great performance improvements and better
debugging facilities. If you maintain a Mopidy client, you should review the
[differences between when.js 2 and 3](https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x)
and the
[when.js debugging guide](https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises).
- All promise rejection values are now of the Error type. This ensures that all
JavaScript VMs will show a useful stack trace if a rejected promise's value
is used to throw an exception. To allow catch clauses to handle different
errors differently, server side errors are of the type `Mopidy.ServerError`,
and connection related errors are of the type `Mopidy.ConnectionError`.
### 0.2.0 (2014-01-04)
- **Backwards incompatible change for Node.js users:**

View File

@ -1,36 +1,60 @@
{
"name": "mopidy",
"version": "0.2.0",
"version": "0.4.0",
"description": "Client lib for controlling a Mopidy music server over a WebSocket",
"keywords": [
"mopidy",
"music",
"client",
"websocket",
"json-rpc"
],
"homepage": "http://www.mopidy.com/",
"bugs": "https://github.com/mopidy/mopidy/issues",
"license": "Apache-2.0",
"author": {
"name": "Stein Magnus Jodal",
"email": "stein.magnus@jodal.no",
"url": "http://www.jodal.no"
},
"contributors": [
{
"name": "Stein Magnus Jodal",
"email": "stein.magnus@jodal.no",
"url": "http://www.jodal.no"
},
{
"name": "Paul Connolley",
"email": "paul.connolley@gmail.com"
}
],
"main": "src/mopidy.js",
"repository": {
"type": "git",
"url": "git://github.com/mopidy/mopidy.git"
},
"main": "src/mopidy.js",
"dependencies": {
"bane": "~1.1.0",
"faye-websocket": "~0.7.2",
"when": "~2.7.1"
},
"devDependencies": {
"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-6"
},
"scripts": {
"test": "grunt test",
"build": "grunt build",
"start": "grunt watch"
},
"dependencies": {
"bane": "~1.1.0",
"faye-websocket": "~0.7.2",
"when": "~3.2.3"
},
"devDependencies": {
"buster": "~0.7.13",
"browserify": "~3",
"grunt": "~0.4.5",
"grunt-buster": "~0.3.1",
"grunt-browserify": "~1.3.2",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-uglify": "~0.5.0",
"grunt-contrib-watch": "~0.6.1",
"phantomjs": "~1.9.7-8"
},
"engines": {
"node": "*"
}
}

View File

@ -9,8 +9,8 @@ function Mopidy(settings) {
return new Mopidy(settings);
}
this._console = this._getConsole(settings || {});
this._settings = this._configure(settings || {});
this._console = this._getConsole();
this._backoffDelay = this._settings.backoffDelayMin;
this._pendingRequests = {};
@ -24,13 +24,41 @@ function Mopidy(settings) {
}
}
Mopidy.ConnectionError = function (message) {
this.name = "ConnectionError";
this.message = message;
};
Mopidy.ConnectionError.prototype = new Error();
Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError;
Mopidy.ServerError = function (message) {
this.name = "ServerError";
this.message = message;
};
Mopidy.ServerError.prototype = new Error();
Mopidy.ServerError.prototype.constructor = Mopidy.ServerError;
Mopidy.WebSocket = websocket.Client;
Mopidy.prototype._getConsole = function (settings) {
if (typeof settings.console !== "undefined") {
return settings.console;
}
var con = typeof console !== "undefined" && console || {};
con.log = con.log || function () {};
con.warn = con.warn || function () {};
con.error = con.error || function () {};
return con;
};
Mopidy.prototype._configure = function (settings) {
var currentHost = (typeof document !== "undefined" &&
document.location.host) || "localhost";
settings.webSocketUrl = settings.webSocketUrl ||
"ws://" + currentHost + "/mopidy/ws/";
"ws://" + currentHost + "/mopidy/ws";
if (settings.autoConnect !== false) {
settings.autoConnect = true;
@ -39,19 +67,18 @@ Mopidy.prototype._configure = function (settings) {
settings.backoffDelayMin = settings.backoffDelayMin || 1000;
settings.backoffDelayMax = settings.backoffDelayMax || 64000;
if (typeof settings.callingConvention === "undefined") {
this._console.warn(
"Mopidy.js is using the default calling convention. The " +
"default will change in the future. You should explicitly " +
"specify which calling convention you use.");
}
settings.callingConvention = (
settings.callingConvention || "by-position-only");
return settings;
};
Mopidy.prototype._getConsole = function () {
var console = typeof console !== "undefined" && console || {};
console.log = console.log || function () {};
console.warn = console.warn || function () {};
console.error = console.error || function () {};
return console;
};
Mopidy.prototype._delegateEvents = function () {
// Remove existing event handlers
this.off("websocket:close");
@ -102,10 +129,9 @@ Mopidy.prototype._cleanup = function (closeEvent) {
Object.keys(this._pendingRequests).forEach(function (requestId) {
var resolver = this._pendingRequests[requestId];
delete this._pendingRequests[requestId];
resolver.reject({
message: "WebSocket closed",
closeEvent: closeEvent
});
var error = new Mopidy.ConnectionError("WebSocket closed");
error.closeEvent = closeEvent;
resolver.reject(error);
}.bind(this));
this.emit("state:offline");
@ -141,33 +167,25 @@ Mopidy.prototype._handleWebSocketError = function (error) {
};
Mopidy.prototype._send = function (message) {
var deferred = when.defer();
switch (this._webSocket.readyState) {
case Mopidy.WebSocket.CONNECTING:
deferred.resolver.reject({
message: "WebSocket is still connecting"
});
break;
return when.reject(
new Mopidy.ConnectionError("WebSocket is still connecting"));
case Mopidy.WebSocket.CLOSING:
deferred.resolver.reject({
message: "WebSocket is closing"
});
break;
return when.reject(
new Mopidy.ConnectionError("WebSocket is closing"));
case Mopidy.WebSocket.CLOSED:
deferred.resolver.reject({
message: "WebSocket is closed"
});
break;
return when.reject(
new Mopidy.ConnectionError("WebSocket is closed"));
default:
var deferred = when.defer();
message.jsonrpc = "2.0";
message.id = this._nextRequestId();
this._pendingRequests[message.id] = deferred.resolver;
this._webSocket.send(JSON.stringify(message));
this.emit("websocket:outgoingMessage", message);
return deferred.promise;
}
return deferred.promise;
};
Mopidy.prototype._nextRequestId = (function () {
@ -208,19 +226,22 @@ Mopidy.prototype._handleResponse = function (responseMessage) {
return;
}
var error;
var resolver = this._pendingRequests[responseMessage.id];
delete this._pendingRequests[responseMessage.id];
if (responseMessage.hasOwnProperty("result")) {
resolver.resolve(responseMessage.result);
} else if (responseMessage.hasOwnProperty("error")) {
resolver.reject(responseMessage.error);
error = new Mopidy.ServerError(responseMessage.error.message);
error.code = responseMessage.error.code;
error.data = responseMessage.error.data;
resolver.reject(error);
this._console.warn("Server returned error:", responseMessage.error);
} else {
resolver.reject({
message: "Response without 'result' or 'error' received",
data: {response: responseMessage}
});
error = new Error("Response without 'result' or 'error' received");
error.data = {response: responseMessage};
resolver.reject(error);
this._console.warn(
"Response without 'result' or 'error' received. Message was:",
responseMessage);
@ -237,18 +258,36 @@ Mopidy.prototype._handleEvent = function (eventMessage) {
Mopidy.prototype._getApiSpec = function () {
return this._send({method: "core.describe"})
.then(this._createApi.bind(this), this._handleWebSocketError)
.then(null, this._handleWebSocketError);
.then(this._createApi.bind(this))
.catch(this._handleWebSocketError);
};
Mopidy.prototype._createApi = function (methods) {
var byPositionOrByName = (
this._settings.callingConvention === "by-position-or-by-name");
var caller = function (method) {
return function () {
var params = Array.prototype.slice.call(arguments);
return this._send({
method: method,
params: params
});
var message = {method: method};
if (arguments.length === 0) {
return this._send(message);
}
if (!byPositionOrByName) {
message.params = Array.prototype.slice.call(arguments);
return this._send(message);
}
if (arguments.length > 1) {
return when.reject(new Error(
"Expected zero arguments, a single array, " +
"or a single object."));
}
if (!Array.isArray(arguments[0]) &&
arguments[0] !== Object(arguments[0])) {
return when.reject(new TypeError(
"Expected an array or an object."));
}
message.params = arguments[0];
return this._send(message);
}.bind(this);
}.bind(this);

View File

@ -33,7 +33,10 @@ buster.testCase("Mopidy", {
close: this.stub(),
send: this.stub()
};
this.mopidy = new Mopidy({webSocket: this.webSocket});
this.mopidy = new Mopidy({
callingConvention: "by-position-or-by-name",
webSocket: this.webSocket
});
},
tearDown: function () {
@ -42,31 +45,86 @@ buster.testCase("Mopidy", {
"constructor": {
"connects when autoConnect is true": function () {
new Mopidy({autoConnect: true});
new Mopidy({
autoConnect: true,
callingConvention: "by-position-or-by-name"
});
var currentHost = typeof document !== "undefined" &&
document.location.host || "localhost";
assert.calledOnceWith(this.webSocketConstructorStub,
"ws://" + currentHost + "/mopidy/ws/");
"ws://" + currentHost + "/mopidy/ws");
},
"does not connect when autoConnect is false": function () {
new Mopidy({autoConnect: false});
new Mopidy({
autoConnect: false,
callingConvention: "by-position-or-by-name"
});
refute.called(this.webSocketConstructorStub);
},
"does not connect when passed a WebSocket": function () {
new Mopidy({webSocket: {}});
new Mopidy({
callingConvention: "by-position-or-by-name",
webSocket: {}
});
refute.called(this.webSocketConstructorStub);
},
"defaults to by-position-only calling convention": function () {
var console = {
warn: function () {}
};
var mopidy = new Mopidy({
console: console,
webSocket: this.webSocket,
});
assert.equals(
mopidy._settings.callingConvention,
"by-position-only");
},
"warns if no calling convention explicitly selected": function () {
var console = {
warn: function () {}
};
var stub = this.stub(console, "warn");
new Mopidy({console: console});
assert.calledOnceWith(
stub,
"Mopidy.js is using the default calling convention. The " +
"default will change in the future. You should explicitly " +
"specify which calling convention you use.");
},
"does not warn if calling convention chosen explicitly": function () {
var console = {
warn: function () {}
};
var stub = this.stub(console, "warn");
new Mopidy({
callingConvention: "by-position-or-by-name",
console: console
});
refute.called(stub);
},
"works without 'new' keyword": function () {
var mopidyConstructor = Mopidy; // To trick jshint into submission
var mopidy = mopidyConstructor({webSocket: {}});
var mopidy = mopidyConstructor({
callingConvention: "by-position-or-by-name",
webSocket: {}
});
assert.isObject(mopidy);
assert(mopidy instanceof Mopidy);
@ -75,7 +133,10 @@ buster.testCase("Mopidy", {
".connect": {
"connects when autoConnect is false": function () {
var mopidy = new Mopidy({autoConnect: false});
var mopidy = new Mopidy({
autoConnect: false,
callingConvention: "by-position-or-by-name"
});
refute.called(this.webSocketConstructorStub);
mopidy.connect();
@ -84,12 +145,15 @@ buster.testCase("Mopidy", {
document.location.host || "localhost";
assert.calledOnceWith(this.webSocketConstructorStub,
"ws://" + currentHost + "/mopidy/ws/");
"ws://" + currentHost + "/mopidy/ws");
},
"does nothing when the WebSocket is open": function () {
this.webSocket.readyState = Mopidy.WebSocket.OPEN;
var mopidy = new Mopidy({webSocket: this.webSocket});
var mopidy = new Mopidy({
callingConvention: "by-position-or-by-name",
webSocket: this.webSocket
});
mopidy.connect();
@ -169,12 +233,18 @@ buster.testCase("Mopidy", {
this.mopidy._cleanup(closeEvent);
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
when.join(promise1, promise2).then(done(function () {
assert(false, "Promises should be rejected");
}), done(function (error) {
assert.equals(error.message, "WebSocket closed");
assert.same(error.closeEvent, closeEvent);
}));
when.settle([promise1, promise2]).done(
done(function (descriptors) {
assert.equals(descriptors.length, 2);
descriptors.forEach(function (d) {
assert.equals(d.state, "rejected");
assert(d.reason instanceof Error);
assert(d.reason instanceof Mopidy.ConnectionError);
assert.equals(d.reason.message, "WebSocket closed");
assert.same(d.reason.closeEvent, closeEvent);
});
})
);
},
"emits 'state:offline' event when done": function () {
@ -388,12 +458,17 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send);
promise.then(done(function () {
assert(false);
}), done(function (error) {
assert.equals(
error.message, "WebSocket is still connecting");
}));
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof Mopidy.ConnectionError);
assert.equals(
error.message, "WebSocket is still connecting");
})
);
},
"immediately rejects request if CLOSING": function (done) {
@ -402,12 +477,16 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send);
promise.then(done(function () {
assert(false);
}), done(function (error) {
assert.equals(
error.message, "WebSocket is closing");
}));
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof Mopidy.ConnectionError);
assert.equals(error.message, "WebSocket is closing");
})
);
},
"immediately rejects request if CLOSED": function (done) {
@ -416,12 +495,16 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send);
promise.then(done(function () {
assert(false);
}), done(function (error) {
assert.equals(
error.message, "WebSocket is closed");
}));
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof Mopidy.ConnectionError);
assert.equals(error.message, "WebSocket is closed");
})
);
}
},
@ -544,7 +627,11 @@ buster.testCase("Mopidy", {
"rejects and logs requests which get errors back": function (done) {
var stub = this.stub(this.mopidy._console, "warn");
var promise = this.mopidy._send({method: "bar"});
var responseError = {message: "Error", data: {}};
var responseError = {
code: -32601,
message: "Method not found",
data: {}
};
var responseMessage = {
jsonrpc: "2.0",
id: Object.keys(this.mopidy._pendingRequests)[0],
@ -555,11 +642,49 @@ buster.testCase("Mopidy", {
assert.calledOnceWith(stub,
"Server returned error:", responseError);
promise.then(done(function () {
assert(false);
}), done(function (error) {
assert.equals(error, responseError);
}));
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert.equals(error.code, responseError.code);
assert.equals(error.message, responseError.message);
assert.equals(error.data, responseError.data);
})
);
},
"rejects and logs requests which get errors without data": function (done) {
var stub = this.stub(this.mopidy._console, "warn");
var promise = this.mopidy._send({method: "bar"});
var responseError = {
code: -32601,
message: "Method not found"
// 'data' key intentionally missing
};
var responseMessage = {
jsonrpc: "2.0",
id: Object.keys(this.mopidy._pendingRequests)[0],
error: responseError
};
this.mopidy._handleResponse(responseMessage);
assert.calledOnceWith(stub,
"Server returned error:", responseError);
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof Mopidy.ServerError);
assert.equals(error.code, responseError.code);
assert.equals(error.message, responseError.message);
refute.defined(error.data);
})
);
},
"rejects and logs responses without result or error": function (done) {
@ -575,14 +700,18 @@ buster.testCase("Mopidy", {
assert.calledOnceWith(stub,
"Response without 'result' or 'error' received. Message was:",
responseMessage);
promise.then(done(function () {
assert(false);
}), done(function (error) {
assert.equals(
error.message,
"Response without 'result' or 'error' received");
assert.equals(error.data.response, responseMessage);
}));
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert.equals(
error.message,
"Response without 'result' or 'error' received");
assert.equals(error.data.response, responseMessage);
})
);
}
},
@ -699,6 +828,137 @@ buster.testCase("Mopidy", {
this.mopidy._createApi({});
assert.calledOnceWith(spy);
},
"by-position-only calling convention": {
setUp: function () {
this.mopidy = new Mopidy({
webSocket: this.webSocket,
callingConvention: "by-position-only"
});
this.mopidy._createApi({
foo: {
params: ["bar", "baz"]
}
});
this.sendStub = this.stub(this.mopidy, "_send");
},
"sends no params if no arguments passed to function": function () {
this.mopidy.foo();
assert.calledOnceWith(this.sendStub, {method: "foo"});
},
"sends messages with function arguments unchanged": function () {
this.mopidy.foo(31, 97);
assert.calledOnceWith(this.sendStub, {
method: "foo",
params: [31, 97]
});
},
},
"by-position-or-by-name calling convention": {
setUp: function () {
this.mopidy = new Mopidy({
webSocket: this.webSocket,
callingConvention: "by-position-or-by-name"
});
this.mopidy._createApi({
foo: {
params: ["bar", "baz"]
}
});
this.sendStub = this.stub(this.mopidy, "_send");
},
"must be turned on manually": function () {
assert.equals(
this.mopidy._settings.callingConvention,
"by-position-or-by-name");
},
"sends no params if no arguments passed to function": function () {
this.mopidy.foo();
assert.calledOnceWith(this.sendStub, {method: "foo"});
},
"sends by-position if argument is a list": function () {
this.mopidy.foo([31, 97]);
assert.calledOnceWith(this.sendStub, {
method: "foo",
params: [31, 97]
});
},
"sends by-name if argument is an object": function () {
this.mopidy.foo({bar: 31, baz: 97});
assert.calledOnceWith(this.sendStub, {
method: "foo",
params: {bar: 31, baz: 97}
});
},
"rejects with error if more than one argument": function (done) {
var promise = this.mopidy.foo([1, 2], {c: 3, d: 4});
refute.called(this.sendStub);
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert.equals(
error.message,
"Expected zero arguments, a single array, " +
"or a single object.");
})
);
},
"rejects with error if string": function (done) {
var promise = this.mopidy.foo("hello");
refute.called(this.sendStub);
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof TypeError);
assert.equals(
error.message, "Expected an array or an object.");
})
);
},
"rejects with error if number": function (done) {
var promise = this.mopidy.foo(1337);
refute.called(this.sendStub);
promise.done(
done(function () {
assert(false);
}),
done(function (error) {
assert(error instanceof Error);
assert(error instanceof TypeError);
assert.equals(
error.message, "Expected an array or an object.");
})
);
}
}
}
});

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals
from distutils.version import StrictVersion as SV
import sys
import warnings
from distutils.version import StrictVersion as SV
import pykka
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.18.3'
__version__ = '0.19.0'

View File

@ -25,9 +25,8 @@ mopidy_args = sys.argv[1:]
sys.argv[1:] = []
from mopidy import commands, ext
from mopidy import config as config_lib
from mopidy.utils import log, path, process, versioning
from mopidy import commands, config as config_lib, ext
from mopidy.utils import encoding, log, path, process, versioning
logger = logging.getLogger(__name__)
@ -150,9 +149,10 @@ def create_file_structures_and_config(args, extensions):
default = config_lib.format_initial(extensions)
path.get_or_create_file(config_file, mkdir=False, content=default)
logger.info('Initialized %s with default config', config_file)
except IOError as e:
logger.warning('Unable to initialize %s with default config: %s',
config_file, e)
except IOError as error:
logger.warning(
'Unable to initialize %s with default config: %s',
config_file, encoding.locale_decode(error))
def check_old_locations():

View File

@ -5,5 +5,6 @@ from .actor import Audio
from .dummy import DummyAudio
from .listener import AudioListener
from .constants import PlaybackState
from .utils import (calculate_duration, create_buffer, millisecond_to_clocktime,
supported_uri_schemes)
from .utils import (
calculate_duration, create_buffer, millisecond_to_clocktime,
supported_uri_schemes)

View File

@ -1,27 +1,30 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gobject
import logging
import gobject
import pygst
pygst.require('0.10')
import gst # noqa
import pykka
from mopidy.audio import playlists, utils
from mopidy.audio.constants import PlaybackState
from mopidy.audio.listener import AudioListener
from mopidy.utils import process
from . import mixers, playlists, utils
from .constants import PlaybackState
from .listener import AudioListener
logger = logging.getLogger(__name__)
mixers.register_mixers()
playlists.register_typefinders()
playlists.register_elements()
_GST_STATE_MAPPING = {
gst.STATE_PLAYING: PlaybackState.PLAYING,
gst.STATE_PAUSED: PlaybackState.PAUSED,
gst.STATE_NULL: PlaybackState.STOPPED}
MB = 1 << 20
@ -50,20 +53,17 @@ class Audio(pykka.ThreadingActor):
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
state = PlaybackState.STOPPED
def __init__(self, config):
def __init__(self, config, mixer):
super(Audio, self).__init__()
self._config = config
self._mixer = mixer
self._target_state = gst.STATE_NULL
self._buffering = False
self._playbin = None
self._signal_ids = {} # {(element, event): signal_id}
self._mixer = None
self._mixer_track = None
self._mixer_scale = None
self._software_mixing = False
self._volume_set = None
self._appsrc = None
self._appsrc_caps = None
self._appsrc_need_data_callback = None
@ -74,8 +74,8 @@ class Audio(pykka.ThreadingActor):
try:
self._setup_playbin()
self._setup_output()
self._setup_visualizer()
self._setup_mixer()
self._setup_visualizer()
self._setup_message_processor()
except gobject.GError as ex:
logger.exception(ex)
@ -100,8 +100,12 @@ class Audio(pykka.ThreadingActor):
playbin = gst.element_factory_make('playbin2')
playbin.set_property('flags', PLAYBIN_FLAGS)
playbin.set_property('buffer-size', 2*1024*1024)
playbin.set_property('buffer-duration', 2*gst.SECOND)
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
self._connect(playbin, 'notify::source', self._on_new_source)
self._connect(playbin, 'source-setup', self._on_source_setup)
self._playbin = playbin
@ -133,6 +137,22 @@ class Audio(pykka.ThreadingActor):
self._appsrc = source
def _on_source_setup(self, element, source):
scheme = 'http'
hostname = self._config['proxy']['hostname']
port = 80
if hasattr(source.props, 'proxy') and hostname:
if self._config['proxy']['port']:
port = self._config['proxy']['port']
if self._config['proxy']['scheme']:
scheme = self._config['proxy']['scheme']
proxy = "%s://%s:%d" % (scheme, hostname, port)
source.set_property('proxy', proxy)
source.set_property('proxy-id', self._config['proxy']['username'])
source.set_property('proxy-pw', self._config['proxy']['password'])
def _appsrc_on_need_data(self, appsrc, gst_length_hint):
length_hint = utils.clocktime_to_millisecond(gst_length_hint)
if self._appsrc_need_data_callback is not None:
@ -153,6 +173,7 @@ class Audio(pykka.ThreadingActor):
def _teardown_playbin(self):
self._disconnect(self._playbin, 'about-to-finish')
self._disconnect(self._playbin, 'notify::source')
self._disconnect(self._playbin, 'source-setup')
self._playbin.set_state(gst.STATE_NULL)
def _setup_output(self):
@ -167,6 +188,23 @@ class Audio(pykka.ThreadingActor):
'Failed to create audio output "%s": %s', output_desc, ex)
process.exit_process()
def _setup_mixer(self):
if self._config['audio']['mixer'] != 'software':
return
self._mixer.audio = self.actor_ref.proxy()
self._connect(self._playbin, 'notify::volume', self._on_mixer_change)
self._connect(self._playbin, 'notify::mute', self._on_mixer_change)
def _on_mixer_change(self, element, gparamspec):
self._mixer.trigger_events_for_changed_values()
def _teardown_mixer(self):
if self._config['audio']['mixer'] != 'software':
return
self._disconnect(self._playbin, 'notify::volume')
self._disconnect(self._playbin, 'notify::mute')
self._mixer.audio = None
def _setup_visualizer(self):
visualizer_element = self._config['audio']['visualizer']
if not visualizer_element:
@ -181,86 +219,6 @@ class Audio(pykka.ThreadingActor):
'Failed to create audio visualizer "%s": %s',
visualizer_element, ex)
def _setup_mixer(self):
mixer_desc = self._config['audio']['mixer']
track_desc = self._config['audio']['mixer_track']
volume = self._config['audio']['mixer_volume']
if mixer_desc is None:
logger.info('Not setting up audio mixer')
return
if mixer_desc == 'software':
self._software_mixing = True
logger.info('Audio mixer is using software mixing')
if volume is not None:
self.set_volume(volume)
logger.info('Audio mixer volume set to %d', volume)
return
try:
mixerbin = gst.parse_bin_from_description(
mixer_desc, ghost_unconnected_pads=False)
except gobject.GError as ex:
logger.warning(
'Failed to create audio mixer "%s": %s', mixer_desc, ex)
return
# We assume that the bin will contain a single mixer.
mixer = mixerbin.get_by_interface(b'GstMixer')
if not mixer:
logger.warning(
'Did not find any audio mixers in "%s"', mixer_desc)
return
if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
logger.warning(
'Setting audio mixer "%s" to READY failed', mixer_desc)
return
track = self._select_mixer_track(mixer, track_desc)
if not track:
logger.warning('Could not find usable audio mixer track')
return
self._mixer = mixer
self._mixer_track = track
self._mixer_scale = (
self._mixer_track.min_volume, self._mixer_track.max_volume)
logger.info(
'Audio mixer set to "%s" using track "%s"',
str(mixer.get_factory().get_name()).decode('utf-8'),
str(track.label).decode('utf-8'))
if volume is not None:
self.set_volume(volume)
logger.info('Audio mixer volume set to %d', volume)
def _select_mixer_track(self, mixer, track_label):
# Ignore tracks without volumes, then look for track with
# label equal to the audio/mixer_track config value, otherwise fallback
# to first usable track hoping the mixer gave them to us in a sensible
# order.
usable_tracks = []
for track in mixer.list_tracks():
if not mixer.get_volume(track):
continue
if track_label and track.label == track_label:
return track
elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
gst.interfaces.MIXER_TRACK_OUTPUT):
usable_tracks.append(track)
if usable_tracks:
return usable_tracks[0]
def _teardown_mixer(self):
if self._mixer is not None:
self._mixer.set_state(gst.STATE_NULL)
def _setup_message_processor(self):
bus = self._playbin.get_bus()
bus.add_signal_watch()
@ -271,27 +229,17 @@ class Audio(pykka.ThreadingActor):
self._disconnect(bus, 'message')
bus.remove_signal_watch()
def _on_message(self, bus, message):
if (message.type == gst.MESSAGE_STATE_CHANGED
and message.src == self._playbin):
old_state, new_state, pending_state = message.parse_state_changed()
self._on_playbin_state_changed(old_state, new_state, pending_state)
elif message.type == gst.MESSAGE_BUFFERING:
percent = message.parse_buffering()
logger.debug('Buffer %d%% full', percent)
elif message.type == gst.MESSAGE_EOS:
def _on_message(self, bus, msg):
if msg.type == gst.MESSAGE_STATE_CHANGED and msg.src == self._playbin:
self._on_playbin_state_changed(*msg.parse_state_changed())
elif msg.type == gst.MESSAGE_BUFFERING:
self._on_buffering(msg.parse_buffering())
elif msg.type == gst.MESSAGE_EOS:
self._on_end_of_stream()
elif message.type == gst.MESSAGE_ERROR:
error, debug = message.parse_error()
logger.error(
'%s Debug message: %s',
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
self.stop_playback()
elif message.type == gst.MESSAGE_WARNING:
error, debug = message.parse_warning()
logger.warning(
'%s Debug message: %s',
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
elif msg.type == gst.MESSAGE_ERROR:
self._on_error(*msg.parse_error())
elif msg.type == gst.MESSAGE_WARNING:
self._on_warning(*msg.parse_warning())
def _on_playbin_state_changed(self, old_state, new_state, pending_state):
if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
@ -307,25 +255,45 @@ class Audio(pykka.ThreadingActor):
if new_state == gst.STATE_READY:
return # Ignore READY state as it's GStreamer specific
if new_state == gst.STATE_PLAYING:
new_state = PlaybackState.PLAYING
elif new_state == gst.STATE_PAUSED:
new_state = PlaybackState.PAUSED
elif new_state == gst.STATE_NULL:
new_state = PlaybackState.STOPPED
new_state = _GST_STATE_MAPPING[new_state]
old_state, self.state = self.state, new_state
target_state = _GST_STATE_MAPPING[self._target_state]
if target_state == new_state:
target_state = None
logger.debug(
'Triggering event: state_changed(old_state=%s, new_state=%s)',
old_state, new_state)
AudioListener.send(
'state_changed', old_state=old_state, new_state=new_state)
'Triggering event: state_changed(old_state=%s, new_state=%s, '
'target_state=%s)', old_state, new_state, target_state)
AudioListener.send('state_changed', old_state=old_state,
new_state=new_state, target_state=target_state)
def _on_buffering(self, percent):
if percent < 10 and not self._buffering:
self._playbin.set_state(gst.STATE_PAUSED)
self._buffering = True
if percent == 100:
self._buffering = False
if self._target_state == gst.STATE_PLAYING:
self._playbin.set_state(gst.STATE_PLAYING)
logger.debug('Buffer %d%% full', percent)
def _on_end_of_stream(self):
logger.debug('Triggering reached_end_of_stream event')
AudioListener.send('reached_end_of_stream')
def _on_error(self, error, debug):
logger.error(
'%s Debug message: %s',
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
self.stop_playback()
def _on_warning(self, error, debug):
logger.warning(
'%s Debug message: %s',
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
def set_uri(self, uri):
"""
Set URI of audio to be played.
@ -447,6 +415,7 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False`
"""
self._buffering = False
return self._set_state(gst.STATE_NULL)
def _set_state(self, state):
@ -470,6 +439,7 @@ class Audio(pykka.ThreadingActor):
:type state: :class:`gst.State`
:rtype: :class:`True` if successfull, else :class:`False`
"""
self._target_state = state
result = self._playbin.set_state(state)
if result == gst.STATE_CHANGE_FAILURE:
logger.warning(
@ -486,108 +456,49 @@ class Audio(pykka.ThreadingActor):
def get_volume(self):
"""
Get volume level of the installed mixer.
Get volume level of the software mixer.
Example values:
0:
Muted.
Minimum volume.
100:
Max volume for given system.
:class:`None`:
No mixer present, so the volume is unknown.
Maximum volume.
:rtype: int in range [0..100] or :class:`None`
:rtype: int in range [0..100]
"""
if self._software_mixing:
return int(round(self._playbin.get_property('volume') * 100))
if self._mixer is None:
return None
volumes = self._mixer.get_volume(self._mixer_track)
avg_volume = float(sum(volumes)) / len(volumes)
internal_scale = (0, 100)
if self._volume_set is not None:
volume_set_on_mixer_scale = self._rescale(
self._volume_set, old=internal_scale, new=self._mixer_scale)
else:
volume_set_on_mixer_scale = None
if volume_set_on_mixer_scale == avg_volume:
return self._volume_set
else:
return self._rescale(
avg_volume, old=self._mixer_scale, new=internal_scale)
return int(round(self._playbin.get_property('volume') * 100))
def set_volume(self, volume):
"""
Set volume level of the installed mixer.
Set volume level of the software mixer.
:param volume: the volume in the range [0..100]
:type volume: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if self._software_mixing:
self._playbin.set_property('volume', volume / 100.0)
return True
if self._mixer is None:
return False
self._volume_set = volume
internal_scale = (0, 100)
volume = self._rescale(
volume, old=internal_scale, new=self._mixer_scale)
volumes = (volume,) * self._mixer_track.num_channels
self._mixer.set_volume(self._mixer_track, volumes)
return self._mixer.get_volume(self._mixer_track) == volumes
def _rescale(self, value, old=None, new=None):
"""Convert value between scales."""
new_min, new_max = new
old_min, old_max = old
if old_min == old_max:
return old_max
scaling = float(new_max - new_min) / (old_max - old_min)
return int(round(scaling * (value - old_min) + new_min))
self._playbin.set_property('volume', volume / 100.0)
return True
def get_mute(self):
"""
Get mute status of the installed mixer.
Get mute status of the software mixer.
:rtype: :class:`True` if muted, :class:`False` if unmuted,
:class:`None` if no mixer is installed.
"""
if self._software_mixing:
return self._playbin.get_property('mute')
if self._mixer_track is None:
return None
return bool(self._mixer_track.flags & gst.interfaces.MIXER_TRACK_MUTE)
return self._playbin.get_property('mute')
def set_mute(self, mute):
"""
Mute or unmute of the installed mixer.
Mute or unmute of the software mixer.
:param mute: Wether to mute the mixer or not.
:param mute: Whether to mute the mixer or not.
:type mute: bool
:rtype: :class:`True` if successful, else :class:`False`
"""
if self._software_mixing:
return self._playbin.set_property('mute', bool(mute))
if self._mixer_track is None:
return False
return self._mixer.set_mute(self._mixer_track, bool(mute))
self._playbin.set_property('mute', bool(mute))
return True
def set_metadata(self, track):
"""

View File

@ -27,17 +27,31 @@ class AudioListener(listener.Listener):
"""
pass
def state_changed(self, old_state, new_state):
def state_changed(self, old_state, new_state, target_state):
"""
Called after the playback state have changed.
Will be called for both immediate and async state changes in GStreamer.
Target state is used to when we should be in the target state, but
temporarily need to switch to an other state. A typical example of this
is buffering. When this happens an event with
`old=PLAYING, new=PAUSED, target=PLAYING` will be emitted. Once we have
caught up a `old=PAUSED, new=PLAYING, target=None` event will be
be generated.
Regular state changes will not have target state set as they are final
states which should be stable.
*MAY* be implemented by actor.
:param old_state: the state before the change
:type old_state: string from :class:`mopidy.core.PlaybackState` field
:param new_state: the state after the change
:type new_state: A :class:`mopidy.core.PlaybackState` field
:type new_state: string from :class:`mopidy.core.PlaybackState` field
:param target_state: the intended state
:type target_state: string from :class:`mopidy.core.PlaybackState`
field or :class:`None` if this is a final state.
"""
pass

View File

@ -1,20 +0,0 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gobject
from .auto import AutoAudioMixer
from .fake import FakeMixer
def register_mixer(mixer_class):
gobject.type_register(mixer_class)
gst.element_register(
mixer_class, mixer_class.__name__.lower(), gst.RANK_MARGINAL)
def register_mixers():
register_mixer(AutoAudioMixer)
register_mixer(FakeMixer)

View File

@ -1,76 +0,0 @@
"""Mixer element that automatically selects the real mixer to use.
Set the :confval:`audio/mixer` config value to ``autoaudiomixer`` to use this
mixer.
"""
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import logging
logger = logging.getLogger(__name__)
# TODO: we might want to add some ranking to the mixers we know about?
class AutoAudioMixer(gst.Bin):
__gstdetails__ = (
'AutoAudioMixer',
'Mixer',
'Element automatically selects a mixer.',
'Mopidy')
def __init__(self):
gst.Bin.__init__(self)
mixer = self._find_mixer()
if mixer:
self.add(mixer)
logger.debug('AutoAudioMixer chose: %s', mixer.get_name())
else:
logger.debug('AutoAudioMixer did not find any usable mixers')
def _find_mixer(self):
registry = gst.registry_get_default()
factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY)
factories.sort(key=lambda f: (-f.get_rank(), f.get_name()))
for factory in factories:
# Avoid sink/srcs that implement mixing.
if factory.get_klass() != 'Generic/Audio':
continue
# Avoid anything that doesn't implement mixing.
elif not factory.has_interface('GstMixer'):
continue
if self._test_mixer(factory):
return factory.create()
return None
def _test_mixer(self, factory):
element = factory.create()
if not element:
return False
try:
result = element.set_state(gst.STATE_READY)
if result != gst.STATE_CHANGE_SUCCESS:
return False
# Trust that the default device is sane and just check tracks.
return self._test_tracks(element)
finally:
element.set_state(gst.STATE_NULL)
def _test_tracks(self, element):
# Only allow elements that have a least one output track.
flags = gst.interfaces.MIXER_TRACK_OUTPUT
for track in element.list_tracks():
if track.flags & flags:
return True
return False

View File

@ -1,49 +0,0 @@
"""Fake mixer for use in tests.
Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this
mixer.
"""
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gobject
import gst
from . import utils
class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
__gstdetails__ = (
'FakeMixer',
'Mixer',
'Fake mixer for use in tests.',
'Mopidy')
track_label = gobject.property(type=str, default='Master')
track_initial_volume = gobject.property(type=int, default=0)
track_min_volume = gobject.property(type=int, default=0)
track_max_volume = gobject.property(type=int, default=100)
track_num_channels = gobject.property(type=int, default=2)
track_flags = gobject.property(type=int, default=(
gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT))
def list_tracks(self):
track = utils.create_track(
self.track_label,
self.track_initial_volume,
self.track_min_volume,
self.track_max_volume,
self.track_num_channels,
self.track_flags)
return [track]
def get_volume(self, track):
return track.volumes
def set_volume(self, track, volumes):
track.volumes = volumes
def set_record(self, track, record):
pass

View File

@ -1,37 +0,0 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gobject
def create_track(label, initial_volume, min_volume, max_volume,
num_channels, flags):
class Track(gst.interfaces.MixerTrack):
def __init__(self):
super(Track, self).__init__()
self.volumes = (initial_volume,) * self.num_channels
@gobject.property
def label(self):
return label
@gobject.property
def min_volume(self):
return min_volume
@gobject.property
def max_volume(self):
return max_volume
@gobject.property
def num_channels(self):
return num_channels
@gobject.property
def flags(self):
return flags
return Track()

View File

@ -1,13 +1,14 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gobject
import ConfigParser as configparser
import io
import gobject
import pygst
pygst.require('0.10')
import gst # noqa
try:
import xml.etree.cElementTree as elementtree
except ImportError:
@ -17,16 +18,16 @@ except ImportError:
# TODO: make detect_FOO_header reusable in general mopidy code.
# i.e. give it just a "peek" like function.
def detect_m3u_header(typefind):
return typefind.peek(0, 8) == b'#EXTM3U\n'
return typefind.peek(0, 7).upper() == b'#EXTM3U'
def detect_pls_header(typefind):
return typefind.peek(0, 11).lower() == b'[playlist]\n'
return typefind.peek(0, 10).lower() == b'[playlist]'
def detect_xspf_header(typefind):
data = typefind.peek(0, 150)
if b'xspf' not in data:
if b'xspf' not in data.lower():
return False
try:
@ -40,7 +41,7 @@ def detect_xspf_header(typefind):
def detect_asx_header(typefind):
data = typefind.peek(0, 50)
if b'asx' not in data:
if b'asx' not in data.lower():
return False
try:
@ -81,6 +82,7 @@ def parse_pls(data):
def parse_xspf(data):
try:
# Last element will be root.
for event, element in elementtree.iterparse(data):
element.tag = element.tag.lower() # normalize
except elementtree.ParseError:
@ -93,14 +95,18 @@ def parse_xspf(data):
def parse_asx(data):
try:
# Last element will be root.
for event, element in elementtree.iterparse(data):
element.tag = element.tag.lower() # normalize
except elementtree.ParseError:
return
for ref in element.findall('entry/ref'):
for ref in element.findall('entry/ref[@href]'):
yield ref.get('href', '').strip()
for entry in element.findall('entry[@href]'):
yield entry.get('href', '').strip()
def parse_urilist(data):
for line in data.readlines():

View File

@ -1,16 +1,16 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import datetime
import os
import time
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy import exceptions
from mopidy.models import Track, Artist, Album
from mopidy.utils import path
from mopidy.models import Album, Artist, Track
from mopidy.utils import encoding, path
class Scanner(object):
@ -90,7 +90,8 @@ class Scanner(object):
message = self._bus.pop()
if message.type == gst.MESSAGE_ERROR:
raise exceptions.ScannerError(message.parse_error()[0])
raise exceptions.ScannerError(
encoding.locale_decode(message.parse_error()[0]))
elif message.type == gst.MESSAGE_EOS:
return tags
elif message.type == gst.MESSAGE_ASYNC_DONE:

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gst # noqa
def calculate_duration(num_samples, sample_rate):

View File

@ -6,6 +6,19 @@ from mopidy import listener
class Backend(object):
"""Backend API
If the backend has problems during initialization it should raise
:exc:`mopidy.exceptions.BackendError` with a descriptive error message.
This will make Mopidy print the error message and exit so that the user can
fix the issue.
:param config: the entire Mopidy configuration
:type config: dict
:param audio: actor proxy for the audio subsystem
:type audio: :class:`pykka.ActorProxy` for :class:`mopidy.audio.Audio`
"""
#: Actor proxy to an instance of :class:`mopidy.audio.Audio`.
#:
#: Should be passed to the backend constructor as the kwarg ``audio``,

View File

@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@ -1,17 +0,0 @@
from __future__ import unicode_literals
from mopidy.backend import (
Backend,
LibraryProvider as BaseLibraryProvider,
PlaybackProvider as BasePlaybackProvider,
PlaylistsProvider as BasePlaylistsProvider)
# Make classes previously residing here available in the old location for
# backwards compatibility with extensions targeting Mopidy < 0.18.
__all__ = [
'Backend',
'BaseLibraryProvider',
'BasePlaybackProvider',
'BasePlaylistsProvider',
]

View File

@ -1,5 +0,0 @@
from __future__ import unicode_literals
# Make classes previously residing here available in the old location for
# backwards compatibility with extensions targeting Mopidy < 0.18.
from mopidy.backend.dummy import * # noqa

View File

@ -1,8 +0,0 @@
from __future__ import unicode_literals
from mopidy.backend import BackendListener
# Make classes previously residing here available in the old location for
# backwards compatibility with extensions targeting Mopidy < 0.18.
__all__ = ['BackendListener']

View File

@ -7,9 +7,10 @@ import os
import sys
import glib
import gobject
from mopidy import config as config_lib
from mopidy import config as config_lib, exceptions
from mopidy.audio import Audio
from mopidy.core import Core
from mopidy.utils import deps, process, versioning
@ -99,7 +100,7 @@ class Command(object):
self._children[name] = command
def add_argument(self, *args, **kwargs):
"""Add am argument to the parser.
"""Add an argument to the parser.
This method takes all the same arguments as the
:class:`argparse.ArgumentParser` version of this method.
@ -260,29 +261,70 @@ class RootCommand(Command):
def run(self, args, config):
loop = gobject.MainLoop()
mixer_class = self.get_mixer_class(config, args.registry['mixer'])
backend_classes = args.registry['backend']
frontend_classes = args.registry['frontend']
try:
audio = self.start_audio(config)
mixer = self.start_mixer(config, mixer_class)
audio = self.start_audio(config, mixer)
backends = self.start_backends(config, backend_classes, audio)
core = self.start_core(audio, backends)
core = self.start_core(mixer, backends)
self.start_frontends(config, frontend_classes, core)
loop.run()
except (exceptions.BackendError,
exceptions.FrontendError,
exceptions.MixerError):
logger.info('Initialization error. Exiting...')
except KeyboardInterrupt:
logger.info('Interrupted. Exiting...')
return
finally:
loop.quit()
self.stop_frontends(frontend_classes)
self.stop_core()
self.stop_backends(backend_classes)
self.stop_audio()
self.stop_mixer(mixer_class)
process.stop_remaining_actors()
def start_audio(self, config):
def get_mixer_class(self, config, mixer_classes):
logger.debug(
'Available Mopidy mixers: %s',
', '.join(m.__name__ for m in mixer_classes) or 'none')
selected_mixers = [
m for m in mixer_classes if m.name == config['audio']['mixer']]
if len(selected_mixers) != 1:
logger.error(
'Did not find unique mixer "%s". Alternatives are: %s',
config['audio']['mixer'],
', '.join([m.name for m in mixer_classes]))
process.exit_process()
return selected_mixers[0]
def start_mixer(self, config, mixer_class):
try:
logger.info('Starting Mopidy mixer: %s', mixer_class.__name__)
mixer = mixer_class.start(config=config).proxy()
self.configure_mixer(config, mixer)
return mixer
except exceptions.MixerError as exc:
logger.error(
'Mixer (%s) initialization error: %s',
mixer_class.__name__, exc.message)
raise
def configure_mixer(self, config, mixer):
volume = config['audio']['mixer_volume']
if volume is not None:
mixer.set_volume(volume)
logger.info('Mixer volume set to %d', volume)
else:
logger.debug('Mixer volume left unchanged')
def start_audio(self, config, mixer):
logger.info('Starting Mopidy audio')
return Audio.start(config=config).proxy()
return Audio.start(config=config, mixer=mixer).proxy()
def start_backends(self, config, backend_classes, audio):
logger.info(
@ -291,14 +333,21 @@ class RootCommand(Command):
backends = []
for backend_class in backend_classes:
backend = backend_class.start(config=config, audio=audio).proxy()
backends.append(backend)
try:
backend = backend_class.start(
config=config, audio=audio).proxy()
backends.append(backend)
except exceptions.BackendError as exc:
logger.error(
'Backend (%s) initialization error: %s',
backend_class.__name__, exc.message)
raise
return backends
def start_core(self, audio, backends):
def start_core(self, mixer, backends):
logger.info('Starting Mopidy core')
return Core.start(audio=audio, backends=backends).proxy()
return Core.start(mixer=mixer, backends=backends).proxy()
def start_frontends(self, config, frontend_classes, core):
logger.info(
@ -306,7 +355,13 @@ class RootCommand(Command):
', '.join(f.__name__ for f in frontend_classes) or 'none')
for frontend_class in frontend_classes:
frontend_class.start(config=config, core=core)
try:
frontend_class.start(config=config, core=core)
except exceptions.FrontendError as exc:
logger.error(
'Frontend (%s) initialization error: %s',
frontend_class.__name__, exc.message)
raise
def stop_frontends(self, frontend_classes):
logger.info('Stopping Mopidy frontends')
@ -326,6 +381,10 @@ class RootCommand(Command):
logger.info('Stopping Mopidy audio')
process.stop_actors_by_class(Audio)
def stop_mixer(self, mixer_class):
logger.info('Stopping Mopidy mixer')
process.stop_actors_by_class(mixer_class)
class ConfigCommand(Command):
help = 'Show currently active configuration.'

View File

@ -15,6 +15,7 @@ from mopidy.utils import path, versioning
logger = logging.getLogger(__name__)
_logging_schema = ConfigSchema('logging')
_logging_schema['color'] = Boolean()
_logging_schema['console_format'] = String()
_logging_schema['debug_format'] = String()
_logging_schema['debug_file'] = Path()
@ -24,7 +25,7 @@ _loglevels_schema = LogLevelConfigSchema('loglevels')
_audio_schema = ConfigSchema('audio')
_audio_schema['mixer'] = String()
_audio_schema['mixer_track'] = String(optional=True)
_audio_schema['mixer_track'] = Deprecated()
_audio_schema['mixer_volume'] = Integer(optional=True, minimum=0, maximum=100)
_audio_schema['output'] = String()
_audio_schema['visualizer'] = String(optional=True)
@ -38,7 +39,7 @@ _proxy_schema['username'] = String(optional=True)
_proxy_schema['password'] = Secret(optional=True)
# NOTE: if multiple outputs ever comes something like LogLevelConfigSchema
#_outputs_schema = config.AudioOutputConfigSchema()
# _outputs_schema = config.AudioOutputConfigSchema()
_schemas = [_logging_schema, _loglevels_schema, _audio_schema, _proxy_schema]

View File

@ -1,126 +0,0 @@
from __future__ import print_function, unicode_literals
import io
import os.path
import sys
from mopidy import config as config_lib, ext
from mopidy.utils import path
def load():
settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
print('Checking %s' % settings_file)
setting_globals = {}
try:
execfile(settings_file, setting_globals)
except Exception as e:
print('Problem loading settings: %s' % e)
return setting_globals
def convert(settings):
config = {}
def helper(confval, setting_name):
if settings.get(setting_name) is not None:
section, key = confval.split('/')
config.setdefault(section, {})[key] = settings[setting_name]
# Perform all the simple mappings using our helper:
helper('logging/console_format', 'CONSOLE_LOG_FORMAT')
helper('logging/debug_format', 'DEBUG_LOG_FORMAT')
helper('logging/debug_file', 'DEBUG_LOG_FILENAME')
helper('audio/mixer', 'MIXER')
helper('audio/mixer_track', 'MIXER_TRACK')
helper('audio/mixer_volume', 'MIXER_VOLUME')
helper('audio/output', 'OUTPUT')
helper('proxy/hostname', 'SPOTIFY_PROXY_HOST')
helper('proxy/port', 'SPOTIFY_PROXY_PORT')
helper('proxy/username', 'SPOTIFY_PROXY_USERNAME')
helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD')
helper('local/media_dir', 'LOCAL_MUSIC_PATH')
helper('local/playlists_dir', 'LOCAL_PLAYLIST_PATH')
helper('spotify/username', 'SPOTIFY_USERNAME')
helper('spotify/password', 'SPOTIFY_PASSWORD')
helper('spotify/bitrate', 'SPOTIFY_BITRATE')
helper('spotify/timeout', 'SPOTIFY_TIMEOUT')
helper('spotify/cache_dir', 'SPOTIFY_CACHE_PATH')
helper('stream/protocols', 'STREAM_PROTOCOLS')
helper('http/hostname', 'HTTP_SERVER_HOSTNAME')
helper('http/port', 'HTTP_SERVER_PORT')
helper('http/static_dir', 'HTTP_SERVER_STATIC_DIR')
helper('mpd/hostname', 'MPD_SERVER_HOSTNAME')
helper('mpd/port', 'MPD_SERVER_PORT')
helper('mpd/password', 'MPD_SERVER_PASSWORD')
helper('mpd/max_connections', 'MPD_SERVER_MAX_CONNECTIONS')
helper('mpd/connection_timeout', 'MPD_SERVER_CONNECTION_TIMEOUT')
helper('mpris/desktop_file', 'DESKTOP_FILE')
helper('scrobbler/username', 'LASTFM_USERNAME')
helper('scrobbler/password', 'LASTFM_PASSWORD')
# Assume FRONTENDS/BACKENDS = None implies all enabled, otherwise disable
# if our module path is missing from the setting.
frontends = settings.get('FRONTENDS')
if frontends is not None:
if 'mopidy.frontends.http.HttpFrontend' not in frontends:
config.setdefault('http', {})['enabled'] = False
if 'mopidy.frontends.mpd.MpdFrontend' not in frontends:
config.setdefault('mpd', {})['enabled'] = False
if 'mopidy.frontends.lastfm.LastfmFrontend' not in frontends:
config.setdefault('scrobbler', {})['enabled'] = False
if 'mopidy.frontends.mpris.MprisFrontend' not in frontends:
config.setdefault('mpris', {})['enabled'] = False
backends = settings.get('BACKENDS')
if backends is not None:
if 'mopidy.backends.local.LocalBackend' not in backends:
config.setdefault('local', {})['enabled'] = False
if 'mopidy.backends.spotify.SpotifyBackend' not in backends:
config.setdefault('spotify', {})['enabled'] = False
if 'mopidy.backends.stream.StreamBackend' not in backends:
config.setdefault('stream', {})['enabled'] = False
return config
def main():
settings = load()
if not settings:
return
config = convert(settings)
known = [
'spotify', 'scrobbler', 'mpd', 'mpris', 'local', 'stream', 'http']
extensions = [e for e in ext.load_extensions() if e.ext_name in known]
print(b'Converted config:\n')
print(config_lib.format(config, extensions))
conf_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
if os.path.exists(conf_file):
print('%s exists, exiting.' % conf_file)
sys.exit(1)
print('Write new config to %s? [yN]' % conf_file, end=' ')
if raw_input() != 'y':
print('Not saving, exiting.')
sys.exit(0)
serialized_config = config_lib.format(config, extensions, display=False)
with io.open(conf_file, 'wb') as filehandle:
filehandle.write(serialized_config)
print('Done.')

View File

@ -1,4 +1,5 @@
[logging]
color = true
console_format = %(levelname)-8s %(message)s
debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s
debug_file = mopidy.log
@ -6,7 +7,6 @@ config_file =
[audio]
mixer = software
mixer_track =
mixer_volume =
output = autoaudiosink
visualizer =

View File

@ -4,8 +4,8 @@ import logging
import re
import socket
from mopidy.utils import path
from mopidy.config import validators
from mopidy.utils import path
def decode(value):
@ -151,7 +151,13 @@ class Boolean(ConfigValue):
true_values = ('1', 'yes', 'true', 'on')
false_values = ('0', 'no', 'false', 'off')
def __init__(self, optional=False):
self._required = not optional
def deserialize(self, value):
validators.validate_required(value, self._required)
if not value:
return None
if value.lower() in self.true_values:
return True
elif value.lower() in self.false_values:
@ -185,6 +191,8 @@ class List(ConfigValue):
return tuple(values)
def serialize(self, value, display=False):
if not value:
return b''
return b'\n ' + b'\n '.join(encode(v) for v in value if v)

View File

@ -5,18 +5,20 @@ import itertools
import pykka
from mopidy import audio, backend
from mopidy import audio, backend, mixer
from mopidy.audio import PlaybackState
from mopidy.core.library import LibraryController
from mopidy.core.listener import CoreListener
from mopidy.core.playback import PlaybackController
from mopidy.core.playlists import PlaylistsController
from mopidy.core.tracklist import TracklistController
from mopidy.utils import versioning
from .library import LibraryController
from .listener import CoreListener
from .playback import PlaybackController
from .playlists import PlaylistsController
from .tracklist import TracklistController
class Core(
pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
mixer.MixerListener):
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
library = None
"""The library controller. An instance of
:class:`mopidy.core.LibraryController`."""
@ -33,7 +35,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
"""The tracklist controller. An instance of
:class:`mopidy.core.TracklistController`."""
def __init__(self, audio=None, backends=None):
def __init__(self, mixer=None, backends=None):
super(Core, self).__init__()
self.backends = Backends(backends)
@ -41,7 +43,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
self.library = LibraryController(backends=self.backends, core=self)
self.playback = PlaybackController(
audio=audio, backends=self.backends, core=self)
mixer=mixer, backends=self.backends, core=self)
self.playlists = PlaylistsController(
backends=self.backends, core=self)
@ -66,14 +68,17 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
def reached_end_of_stream(self):
self.playback.on_end_of_track()
def state_changed(self, old_state, new_state):
def state_changed(self, old_state, new_state, target_state):
# XXX: This is a temporary fix for issue #232 while we wait for a more
# permanent solution with the implementation of issue #234. When the
# Spotify play token is lost, the Spotify backend pauses audio
# playback, but mopidy.core doesn't know this, so we need to update
# mopidy.core's state to match the actual state in mopidy.audio. If we
# don't do this, clients will think that we're still playing.
if (new_state == PlaybackState.PAUSED
# We ignore cases when target state is set as this is buffering
# updates (at least for now) and we need to get #234 fixed...
if (new_state == PlaybackState.PAUSED and not target_state
and self.playback.state != PlaybackState.PAUSED):
self.playback.state = new_state
self.playback._trigger_track_playback_paused()
@ -82,6 +87,14 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
# Forward event from backend to frontends
CoreListener.send('playlists_loaded')
def volume_changed(self, volume):
# Forward event from mixer to frontends
CoreListener.send('volume_changed', volume=volume)
def mute_changed(self, mute):
# Forward event from mixer to frontends
CoreListener.send('mute_changed', mute=mute)
class Backends(list):
def __init__(self, backends):
@ -93,26 +106,26 @@ class Backends(list):
self.with_playlists = collections.OrderedDict()
backends_by_scheme = {}
name = lambda backend: backend.actor_ref.actor_class.__name__
name = lambda b: b.actor_ref.actor_class.__name__
for backend in backends:
has_library = backend.has_library().get()
has_library_browse = backend.has_library_browse().get()
has_playback = backend.has_playback().get()
has_playlists = backend.has_playlists().get()
for b in backends:
has_library = b.has_library().get()
has_library_browse = b.has_library_browse().get()
has_playback = b.has_playback().get()
has_playlists = b.has_playlists().get()
for scheme in backend.uri_schemes.get():
for scheme in b.uri_schemes.get():
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
) % (scheme, name(b), name(backends_by_scheme[scheme]))
backends_by_scheme[scheme] = b
if has_library:
self.with_library[scheme] = backend
self.with_library[scheme] = b
if has_library_browse:
self.with_library_browse[scheme] = backend
self.with_library_browse[scheme] = b
if has_playback:
self.with_playback[scheme] = backend
self.with_playback[scheme] = b
if has_playlists:
self.with_playlists[scheme] = backend
self.with_playlists[scheme] = b

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
import collections
import operator
import urlparse
import pykka
@ -62,7 +63,8 @@ class LibraryController(object):
"""
if uri is None:
backends = self.backends.with_library_browse.values()
return [b.library.root_directory.get() for b in backends]
unique_dirs = {b.library.root_directory.get() for b in backends}
return sorted(unique_dirs, key=operator.attrgetter('name'))
scheme = urlparse.urlparse(uri).scheme
backend = self.backends.with_library_browse.get(scheme)

View File

@ -4,8 +4,7 @@ import logging
import urlparse
from mopidy.audio import PlaybackState
from . import listener
from mopidy.core import listener
logger = logging.getLogger(__name__)
@ -14,8 +13,8 @@ logger = logging.getLogger(__name__)
class PlaybackController(object):
pykka_traversable = True
def __init__(self, audio, backends, core):
self.audio = audio
def __init__(self, mixer, backends, core):
self.mixer = mixer
self.backends = backends
self.core = core
@ -30,7 +29,7 @@ class PlaybackController(object):
uri_scheme = urlparse.urlparse(uri).scheme
return self.backends.with_playback.get(uri_scheme, None)
### Properties
# Properties
def get_current_tl_track(self):
return self.current_tl_track
@ -89,45 +88,43 @@ class PlaybackController(object):
"""Time position in milliseconds."""
def get_volume(self):
if self.audio:
return self.audio.get_volume().get()
if self.mixer:
return self.mixer.get_volume().get()
else:
# For testing
return self._volume
def set_volume(self, volume):
if self.audio:
self.audio.set_volume(volume)
if self.mixer:
self.mixer.set_volume(volume)
else:
# For testing
self._volume = volume
self._trigger_volume_changed(volume)
volume = property(get_volume, set_volume)
"""Volume as int in range [0..100] or :class:`None`"""
"""Volume as int in range [0..100] or :class:`None` if unknown. The volume
scale is linear.
"""
def get_mute(self):
if self.audio:
return self.audio.get_mute().get()
if self.mixer:
return self.mixer.get_mute().get()
else:
# For testing
return self._mute
def set_mute(self, value):
value = bool(value)
if self.audio:
self.audio.set_mute(value)
if self.mixer:
self.mixer.set_mute(value)
else:
# For testing
self._mute = value
self._trigger_mute_changed(value)
mute = property(get_mute, set_mute)
"""Mute state as a :class:`True` if muted, :class:`False` otherwise"""
### Methods
# Methods
def change_track(self, tl_track, on_error_step=1):
"""
@ -352,14 +349,6 @@ class PlaybackController(object):
'playback_state_changed',
old_state=old_state, new_state=new_state)
def _trigger_volume_changed(self, volume):
logger.debug('Triggering volume changed event')
listener.CoreListener.send('volume_changed', volume=volume)
def _trigger_mute_changed(self, mute):
logger.debug('Triggering mute changed event')
listener.CoreListener.send('mute_changed', mute=mute)
def _trigger_seeked(self, time_position):
logger.debug('Triggering seeked event')
listener.CoreListener.send('seeked', time_position=time_position)

View File

@ -4,10 +4,9 @@ import collections
import logging
import random
from mopidy.core import listener
from mopidy.models import TlTrack
from . import listener
logger = logging.getLogger(__name__)
@ -23,7 +22,7 @@ class TracklistController(object):
self._shuffled = []
### Properties
# Properties
def get_tl_tracks(self):
return self._tl_tracks[:]
@ -136,7 +135,7 @@ class TracklistController(object):
Playback continues after current song.
"""
### Methods
# Methods
def index(self, tl_track):
"""

View File

@ -16,9 +16,21 @@ class MopidyException(Exception):
self._message = message
class BackendError(MopidyException):
pass
class ExtensionError(MopidyException):
pass
class FrontendError(MopidyException):
pass
class MixerError(MopidyException):
pass
class ScannerError(MopidyException):
pass

View File

@ -2,10 +2,10 @@ from __future__ import unicode_literals
import collections
import logging
import pkg_resources
from mopidy import exceptions
from mopidy import config as config_lib
from mopidy import config as config_lib, exceptions
logger = logging.getLogger(__name__)
@ -99,42 +99,6 @@ class Extension(object):
:param registry: the extension registry
:type registry: :class:`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
.. deprecated:: 0.18
Use :meth:`setup` instead.
:returns: list of :class:`pykka.Actor` subclasses
"""
return []
def get_backend_classes(self):
"""List of backend actor classes
.. deprecated:: 0.18
Use :meth:`setup` instead.
:returns: list of :class:`~mopidy.backend.Backend` subclasses
"""
return []
def register_gstreamer_elements(self):
"""Hook for registering custom GStreamer elements.
.. deprecated:: 0.18
Use :meth:`setup` instead.
:returns: :class:`None`
"""
pass

View File

@ -1,40 +1,48 @@
from __future__ import unicode_literals
import logging
import os
import mopidy
from mopidy import config, exceptions, ext
from mopidy import config as config_lib, exceptions, ext
logger = logging.getLogger(__name__)
class Extension(ext.Extension):
dist_name = 'Mopidy-HTTP'
ext_name = 'http'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
return config_lib.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['hostname'] = config.Hostname()
schema['port'] = config.Port()
schema['static_dir'] = config.Path(optional=True)
schema['zeroconf'] = config.String(optional=True)
schema['hostname'] = config_lib.Hostname()
schema['port'] = config_lib.Port()
schema['static_dir'] = config_lib.Path(optional=True)
schema['zeroconf'] = config_lib.String(optional=True)
return schema
def validate_environment(self):
try:
import cherrypy # noqa
import tornado.web # noqa
except ImportError as e:
raise exceptions.ExtensionError('cherrypy library not found', e)
try:
import ws4py # noqa
except ImportError as e:
raise exceptions.ExtensionError('ws4py library not found', e)
raise exceptions.ExtensionError('tornado library not found', e)
def setup(self, registry):
from .actor import HttpFrontend
from .handlers import make_mopidy_app_factory
HttpFrontend.apps = registry['http:app']
HttpFrontend.statics = registry['http:static']
registry.add('frontend', HttpFrontend)
registry.add('http:app', {
'name': 'mopidy',
'factory': make_mopidy_app_factory(
registry['http:app'], registry['http:static']),
})

View File

@ -1,129 +1,182 @@
from __future__ import unicode_literals
import logging
import json
import logging
import os
import threading
import cherrypy
import pykka
from ws4py.messaging import TextMessage
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
from mopidy import models, zeroconf
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.web
import tornado.websocket
from mopidy import exceptions, models, zeroconf
from mopidy.core import CoreListener
from . import ws
from mopidy.http import handlers
from mopidy.utils import encoding, formatting, network
logger = logging.getLogger(__name__)
class HttpFrontend(pykka.ThreadingActor, CoreListener):
apps = []
statics = []
def __init__(self, config, core):
super(HttpFrontend, self).__init__()
self.config = config
self.core = core
self.hostname = config['http']['hostname']
self.hostname = network.format_hostname(config['http']['hostname'])
self.port = config['http']['port']
tornado_hostname = config['http']['hostname']
if tornado_hostname == '::':
tornado_hostname = None
try:
logger.debug('Starting HTTP server')
sockets = tornado.netutil.bind_sockets(self.port, tornado_hostname)
self.server = HttpServer(
config=config, core=core, sockets=sockets,
apps=self.apps, statics=self.statics)
except IOError as error:
raise exceptions.FrontendError(
'HTTP server startup failed: %s' %
encoding.locale_decode(error))
self.zeroconf_name = config['http']['zeroconf']
self.zeroconf_service = None
self._setup_server()
self._setup_websocket_plugin()
app = self._create_app()
self._setup_logging(app)
def _setup_server(self):
cherrypy.config.update({
'engine.autoreload_on': False,
'server.socket_host': self.hostname,
'server.socket_port': self.port,
})
def _setup_websocket_plugin(self):
WebSocketPlugin(cherrypy.engine).subscribe()
cherrypy.tools.websocket = WebSocketTool()
def _create_app(self):
root = RootResource()
root.mopidy = MopidyResource()
root.mopidy.ws = ws.WebSocketResource(self.core)
if self.config['http']['static_dir']:
static_dir = self.config['http']['static_dir']
else:
static_dir = os.path.join(os.path.dirname(__file__), 'data')
logger.debug('HTTP server will serve "%s" at /', static_dir)
mopidy_dir = os.path.join(os.path.dirname(__file__), 'data')
favicon = os.path.join(mopidy_dir, 'favicon.png')
config = {
b'/': {
'tools.staticdir.on': True,
'tools.staticdir.index': 'index.html',
'tools.staticdir.dir': static_dir,
},
b'/favicon.ico': {
'tools.staticfile.on': True,
'tools.staticfile.filename': favicon,
},
b'/mopidy': {
'tools.staticdir.on': True,
'tools.staticdir.index': 'mopidy.html',
'tools.staticdir.dir': mopidy_dir,
},
b'/mopidy/ws': {
'tools.websocket.on': True,
'tools.websocket.handler_cls': ws.WebSocketHandler,
},
}
return cherrypy.tree.mount(root, '/', config)
def _setup_logging(self, app):
cherrypy.log.access_log.setLevel(logging.NOTSET)
cherrypy.log.error_log.setLevel(logging.NOTSET)
cherrypy.log.screen = False
app.log.access_log.setLevel(logging.NOTSET)
app.log.error_log.setLevel(logging.NOTSET)
self.zeroconf_http = None
self.zeroconf_mopidy_http = None
def on_start(self):
logger.debug('Starting HTTP server')
cherrypy.engine.start()
logger.info('HTTP server running at %s', cherrypy.server.base())
logger.info(
'HTTP server running at [%s]:%s', self.hostname, self.port)
self.server.start()
if self.zeroconf_name:
self.zeroconf_service = zeroconf.Zeroconf(
self.zeroconf_http = zeroconf.Zeroconf(
stype='_http._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.debug(
'Registered HTTP with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.debug('Registering HTTP with Zeroconf failed.')
self.zeroconf_mopidy_http = zeroconf.Zeroconf(
stype='_mopidy-http._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
self.zeroconf_http.publish()
self.zeroconf_mopidy_http.publish()
def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
if self.zeroconf_http:
self.zeroconf_http.unpublish()
if self.zeroconf_mopidy_http:
self.zeroconf_mopidy_http.unpublish()
logger.debug('Stopping HTTP server')
cherrypy.engine.exit()
logger.info('Stopped HTTP server')
self.server.stop()
def on_event(self, name, **data):
event = data
event['event'] = name
message = json.dumps(event, cls=models.ModelJSONEncoder)
cherrypy.engine.publish('websocket-broadcast', TextMessage(message))
on_event(name, **data)
class RootResource(object):
pass
def on_event(name, **data):
event = data
event['event'] = name
message = json.dumps(event, cls=models.ModelJSONEncoder)
handlers.WebSocketHandler.broadcast(message)
class MopidyResource(object):
pass
class HttpServer(threading.Thread):
name = 'HttpServer'
def __init__(self, config, core, sockets, apps, statics):
super(HttpServer, self).__init__()
self.config = config
self.core = core
self.sockets = sockets
self.apps = apps
self.statics = statics
self.app = None
self.server = None
def run(self):
self.app = tornado.web.Application(self._get_request_handlers())
self.server = tornado.httpserver.HTTPServer(self.app)
self.server.add_sockets(self.sockets)
tornado.ioloop.IOLoop.instance().start()
logger.debug('Stopped HTTP server')
def stop(self):
logger.debug('Stopping HTTP server')
tornado.ioloop.IOLoop.instance().add_callback(
tornado.ioloop.IOLoop.instance().stop)
def _get_request_handlers(self):
request_handlers = []
request_handlers.extend(self._get_app_request_handlers())
request_handlers.extend(self._get_static_request_handlers())
request_handlers.extend(self._get_mopidy_request_handlers())
logger.debug(
'HTTP routes from extensions: %s',
formatting.indent('\n'.join(
'%r: %r' % (r[0], r[1]) for r in request_handlers)))
return request_handlers
def _get_app_request_handlers(self):
result = []
for app in self.apps:
result.append((
r'/%s' % app['name'],
handlers.AddSlashHandler
))
request_handlers = app['factory'](self.config, self.core)
for handler in request_handlers:
handler = list(handler)
handler[0] = '/%s%s' % (app['name'], handler[0])
result.append(tuple(handler))
logger.debug('Loaded HTTP extension: %s', app['name'])
return result
def _get_static_request_handlers(self):
result = []
for static in self.statics:
result.append((
r'/%s' % static['name'],
handlers.AddSlashHandler
))
result.append((
r'/%s/(.*)' % static['name'],
handlers.StaticFileHandler,
{
'path': static['path'],
'default_filename': 'index.html'
}
))
logger.debug('Loaded static HTTP extension: %s', static['name'])
return result
def _get_mopidy_request_handlers(self):
# Either default Mopidy or user defined path to files
static_dir = self.config['http']['static_dir']
if static_dir and not os.path.exists(static_dir):
logger.warning(
'Configured http/static_dir %s does not exist. '
'Falling back to default HTTP handler.', static_dir)
static_dir = None
if static_dir:
return [(r'/(.*)', handlers.StaticFileHandler, {
'path': self.config['http']['static_dir'],
'default_filename': 'index.html',
})]
else:
return [(r'/', tornado.web.RedirectHandler, {
'url': '/mopidy/',
'permanent': False,
})]

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mopidy</title>
<link rel="stylesheet" type="text/css" href="mopidy.css">
</head>
<body>
<div class="box focus">
<h1>Mopidy</h1>
<p>This web server is a part of the Mopidy music server. To learn more
about Mopidy, please visit
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
</div>
<div class="box">
<h2>Web clients</h2>
<ul>
{% for app in apps %}
<li><a href="/{{ url_escape(app) }}/">{{ escape(app) }}</a></li>
{% end %}
</ul>
<p>Web clients which are installed as Mopidy extensions will
automatically appear here.</p>
</div>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mopidy HTTP frontend</title>
<link rel="stylesheet" type="text/css" href="mopidy.css">
</head>
<body>
<div class="box focus">
<h1>Mopidy HTTP&nbsp;frontend</h1>
<p>This web server is a part of the music server Mopidy. To learn more
about Mopidy, please visit
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
</div>
<div class="box">
<h2>Static content serving</h2>
<p>To see your own content instead of this placeholder page, change the
setting <tt>HTTP_SERVER_STATIC_DIR</tt> to point to the directory
containing your static files. This can be used to host e.g. a pure
HTML/CSS/JavaScript Mopidy client.</p>
<p>If you replace this page with your own content, the Mopidy resources
at <a href="/mopidy/">/mopidy/</a> will still be available.</p>
</div>
</body>
</html>

View File

@ -1,40 +1,17 @@
html {
background: #e8ecef;
background: #f8f8f8;
color: #555;
font-family: "Droid Serif", "Georgia", "Times New Roman", "Palatino",
"Hoefler Text", "Baskerville", serif;
font-size: 150%;
font-family: Geneva, Tahoma, Verdana, sans-serif;
line-height: 1.4em;
}
body {
max-width: 20em;
max-width: 600px;
margin: 0 auto;
}
div.box {
background: white;
border-radius: 5px;
box-shadow: 5px 5px 5px #d8dcdf;
margin: 2em 0;
padding: 1em;
}
div.box.focus {
background: #465158;
color: #e8ecef;
}
div.icon {
float: right;
}
h1, h2 {
font-family: "Ubuntu", "Arial", "Helvetica", "Lucida Grande",
"Verdana", "Gill Sans", sans-serif;
font-weight: 500;
line-height: 1.1em;
}
h2 {
margin: 0.2em 0 0;
}
p.next {
text-align: right;
}
a {
color: #555;
text-decoration: none;
@ -43,20 +20,18 @@ a {
img {
border: 0;
}
code, pre {
font-family: "Droid Sans Mono", Menlo, Courier New, Courier, Mono, monospace;
font-size: 9pt;
line-height: 1.2em;
padding: 0.5em 1em;
margin: 1em 0;
white-space: pre;
overflow: auto;
.box {
background: white;
box-shadow: 0px 5px 5px #f0f0f0;
margin: 1em;
padding: 1em;
}
.box code,
.box pre {
background: #e8ecef;
color: #555;
.box.focus {
background: #465158;
color: #e8ecef;
}
.box a {
color: #465158;
}
@ -66,10 +41,3 @@ code, pre {
.box.focus a {
color: #e8ecef;
}
.center {
text-align: center;
}
#ws-console {
height: 200px;
overflow: auto;
}

View File

@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mopidy HTTP frontend</title>
<link rel="stylesheet" type="text/css" href="mopidy.css">
</head>
<body>
<div class="box focus">
<h1>Mopidy HTTP&nbsp;frontend</h1>
<p>This web server is a part of the music server Mopidy. To learn more
about Mopidy, please visit <a
href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
</div>
<div class="box">
<h2>WebSocket endpoint</h2>
<p>Mopidy has a WebSocket endpoint at <tt>/mopidy/ws/</tt>. You can use
this end point to access Mopidy's full API, and to get notified about
events happening in Mopidy.</p>
</div>
<div class="box">
<h2>Example</h2>
<p>Here you can see events arriving from Mopidy in real time:</p>
<pre id="ws-console"></pre>
<p>Nothing to see? Try playing a track using your MPD client.</p>
</div>
<div class="box focus">
<h2>Documentation</h2>
<p>For more information, please refer to the Mopidy documentation at
<a href="http://docs.mopidy.com/">docs.mopidy.com</a>.</p>
</div>
<script type="text/javascript">
var ws = new WebSocket("ws://" + document.location.host + "/mopidy/ws/");
ws.onmessage = function (message) {
var console = document.getElementById('ws-console');
var newLine = (new Date()).toLocaleTimeString() + ": " +
message.data + "\n";
console.innerHTML = newLine + console.innerHTML;
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

180
mopidy/http/handlers.py Normal file
View File

@ -0,0 +1,180 @@
from __future__ import unicode_literals
import logging
import os
import tornado.escape
import tornado.web
import tornado.websocket
import mopidy
from mopidy import core, models
from mopidy.utils import jsonrpc
logger = logging.getLogger(__name__)
def make_mopidy_app_factory(apps, statics):
def mopidy_app_factory(config, core):
return [
(r'/ws/?', WebSocketHandler, {
'core': core,
}),
(r'/rpc', JsonRpcHandler, {
'core': core,
}),
(r'/(.+)', StaticFileHandler, {
'path': os.path.join(os.path.dirname(__file__), 'data'),
}),
(r'/', ClientListHandler, {
'apps': apps,
'statics': statics,
}),
]
return mopidy_app_factory
def make_jsonrpc_wrapper(core_actor):
inspector = jsonrpc.JsonRpcInspector(
objects={
'core.get_uri_schemes': core.Core.get_uri_schemes,
'core.get_version': core.Core.get_version,
'core.library': core.LibraryController,
'core.playback': core.PlaybackController,
'core.playlists': core.PlaylistsController,
'core.tracklist': core.TracklistController,
})
return jsonrpc.JsonRpcWrapper(
objects={
'core.describe': inspector.describe,
'core.get_uri_schemes': core_actor.get_uri_schemes,
'core.get_version': core_actor.get_version,
'core.library': core_actor.library,
'core.playback': core_actor.playback,
'core.playlists': core_actor.playlists,
'core.tracklist': core_actor.tracklist,
},
decoders=[models.model_json_decoder],
encoders=[models.ModelJSONEncoder]
)
class WebSocketHandler(tornado.websocket.WebSocketHandler):
# XXX This set is shared by all WebSocketHandler objects. This isn't
# optimal, but there's currently no use case for having more than one of
# these anyway.
clients = set()
@classmethod
def broadcast(cls, msg):
for client in cls.clients:
client.write_message(msg)
def initialize(self, core):
self.jsonrpc = make_jsonrpc_wrapper(core)
def open(self):
self.set_nodelay(True)
self.clients.add(self)
logger.debug(
'New WebSocket connection from %s', self.request.remote_ip)
def on_close(self):
self.clients.discard(self)
logger.debug(
'Closed WebSocket connection from %s',
self.request.remote_ip)
def on_message(self, message):
if not message:
return
logger.debug(
'Received WebSocket message from %s: %r',
self.request.remote_ip, message)
try:
response = self.jsonrpc.handle_json(
tornado.escape.native_str(message))
if response and self.write_message(response):
logger.debug(
'Sent WebSocket message to %s: %r',
self.request.remote_ip, response)
except Exception as e:
logger.error('WebSocket request error: %s', e)
self.close()
def set_mopidy_headers(request_handler):
request_handler.set_header('Cache-Control', 'no-cache')
request_handler.set_header(
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
class JsonRpcHandler(tornado.web.RequestHandler):
def initialize(self, core):
self.jsonrpc = make_jsonrpc_wrapper(core)
def head(self):
self.set_extra_headers()
self.finish()
def post(self):
data = self.request.body
if not data:
return
logger.debug(
'Received RPC message from %s: %r', self.request.remote_ip, data)
try:
self.set_extra_headers()
response = self.jsonrpc.handle_json(
tornado.escape.native_str(data))
if response and self.write(response):
logger.debug(
'Sent RPC message to %s: %r',
self.request.remote_ip, response)
except Exception as e:
logger.error('HTTP JSON-RPC request error:', e)
self.write_error(500)
def set_extra_headers(self):
set_mopidy_headers(self)
self.set_header('Accept', 'application/json')
self.set_header('Content-Type', 'application/json; utf-8')
class ClientListHandler(tornado.web.RequestHandler):
def initialize(self, apps, statics):
self.apps = apps
self.statics = statics
def get_template_path(self):
return os.path.dirname(__file__)
def get(self):
set_mopidy_headers(self)
names = set()
for app in self.apps:
names.add(app['name'])
for static in self.statics:
names.add(static['name'])
names.discard('mopidy')
self.render('data/clients.html', apps=sorted(list(names)))
class StaticFileHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
set_mopidy_headers(self)
class AddSlashHandler(tornado.web.RequestHandler):
@tornado.web.addslash
def prepare(self):
return super(AddSlashHandler, self).prepare()

View File

@ -1,93 +0,0 @@
from __future__ import unicode_literals
import logging
import socket
import cherrypy
import ws4py.websocket
from mopidy import core, models
from mopidy.utils import jsonrpc
logger = logging.getLogger(__name__)
class WebSocketResource(object):
def __init__(self, core_proxy):
self._core = core_proxy
inspector = jsonrpc.JsonRpcInspector(
objects={
'core.get_uri_schemes': core.Core.get_uri_schemes,
'core.get_version': core.Core.get_version,
'core.library': core.LibraryController,
'core.playback': core.PlaybackController,
'core.playlists': core.PlaylistsController,
'core.tracklist': core.TracklistController,
})
self.jsonrpc = jsonrpc.JsonRpcWrapper(
objects={
'core.describe': inspector.describe,
'core.get_uri_schemes': self._core.get_uri_schemes,
'core.get_version': self._core.get_version,
'core.library': self._core.library,
'core.playback': self._core.playback,
'core.playlists': self._core.playlists,
'core.tracklist': self._core.tracklist,
},
decoders=[models.model_json_decoder],
encoders=[models.ModelJSONEncoder])
@cherrypy.expose
def index(self):
logger.debug('WebSocket handler created')
cherrypy.request.ws_handler.jsonrpc = self.jsonrpc
class _WebSocket(ws4py.websocket.WebSocket):
"""Sub-class ws4py WebSocket with better error handling."""
def send(self, *args, **kwargs):
try:
super(_WebSocket, self).send(*args, **kwargs)
return True
except socket.error as e:
logger.warning('Send message failed: %s', e)
# This isn't really correct, but its the only way to break of out
# the loop in run and trick ws4py into cleaning up.
self.client_terminated = self.server_terminated = True
return False
def close(self, *args, **kwargs):
try:
super(_WebSocket, self).close(*args, **kwargs)
except socket.error as e:
logger.warning('Closing WebSocket failed: %s', e)
class WebSocketHandler(_WebSocket):
def opened(self):
remote = cherrypy.request.remote
logger.debug(
'New WebSocket connection from %s:%d',
remote.ip, remote.port)
def closed(self, code, reason=None):
remote = cherrypy.request.remote
logger.debug(
'Closed WebSocket connection from %s:%d '
'with code %s and reason %r',
remote.ip, remote.port, code, reason)
def received_message(self, request):
remote = cherrypy.request.remote
request = str(request)
logger.debug(
'Received WebSocket message from %s:%d: %r',
remote.ip, remote.port, request)
response = self.jsonrpc.handle_json(request)
if response and self.send(response):
logger.debug(
'Sent WebSocket message to %s:%d: %r',
remote.ip, remote.port, response)

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import logging
import gobject
import pykka
logger = logging.getLogger(__name__)

View File

@ -1,16 +1,15 @@
from __future__ import unicode_literals
import logging
import os
import pykka
from mopidy import backend
from mopidy.utils import encoding, path
from mopidy.local import storage
from mopidy.local.library import LocalLibraryProvider
from mopidy.local.playback import LocalPlaybackProvider
from mopidy.local.playlists import LocalPlaylistsProvider
from .library import LocalLibraryProvider
from .playback import LocalPlaybackProvider
from .playlists import LocalPlaylistsProvider
logger = logging.getLogger(__name__)
@ -24,7 +23,7 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
self.config = config
self.check_dirs_and_files()
storage.check_dirs_and_files(config)
libraries = dict((l.name, l) for l in self.libraries)
library_name = config['local']['library']
@ -39,23 +38,3 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
self.playlists = LocalPlaylistsProvider(backend=self)
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:
logger.warning(
'Could not create local playlists dir: %s',
encoding.locale_decode(error))

View File

@ -6,9 +6,9 @@ import time
from mopidy import commands, exceptions
from mopidy.audio import scan
from mopidy.local import translator
from mopidy.utils import path
from . import translator
logger = logging.getLogger(__name__)
@ -65,59 +65,59 @@ class ScanCommand(commands.Command):
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(
file_ext.lower() for file_ext in excluded_file_extensions)
excluded_file_extensions = tuple(
bytes(file_ext.lower()) for file_ext in excluded_file_extensions)
library = _get_library(args, config)
uri_path_mapping = {}
uris_in_library = set()
uris_to_update = set()
uris_to_remove = set()
file_mtimes = path.find_mtimes(media_dir)
logger.info('Found %d files in media_dir.', len(file_mtimes))
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:
stat = os.stat(uri_path_mapping[track.uri])
if int(stat.st_mtime) > track.last_modified:
uris_to_update.add(track.uri)
uris_in_library.add(track.uri)
except OSError:
abspath = translator.local_track_uri_to_path(track.uri, media_dir)
mtime = file_mtimes.pop(abspath, None)
if mtime is None:
logger.debug('Missing file %s', track.uri)
uris_to_remove.add(track.uri)
elif mtime > track.last_modified:
uris_in_library.add(track.uri)
logger.info('Removing %d missing tracks.', len(uris_to_remove))
for uri in uris_to_remove:
library.remove(uri)
logger.info('Checking %s for unknown tracks.', media_dir)
for relpath in path.find_files(media_dir):
for abspath in file_mtimes:
relpath = os.path.relpath(abspath, 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:
if relpath.lower().endswith(excluded_file_extensions):
logger.debug('Skipped %s: File extension excluded.', uri)
continue
if uri not in uris_in_library:
uris_to_update.add(uri)
uri_path_mapping[uri] = os.path.join(media_dir, relpath)
uris_to_update.add(uri)
logger.info('Found %d unknown tracks.', len(uris_to_update))
logger.info(
'Found %d tracks which need to be updated.', len(uris_to_update))
logger.info('Scanning...')
uris_to_update = sorted(uris_to_update)[:args.limit]
uris_to_update = sorted(uris_to_update, key=lambda v: v.lower())
uris_to_update = uris_to_update[:args.limit]
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]))
relpath = translator.local_track_uri_to_path(uri, media_dir)
file_uri = path.path_to_uri(os.path.join(media_dir, relpath))
data = scanner.scan(file_uri)
track = scan.audio_data_to_track(data).copy(uri=uri)
library.add(track)
logger.debug('Added %s', track.uri)

View File

@ -7,6 +7,7 @@ playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
scan_timeout = 1000
scan_flush_threshold = 1000
excluded_file_extensions =
.directory
.html
.jpeg
.jpg

View File

@ -12,18 +12,29 @@ import time
import mopidy
from mopidy import local, models
from mopidy.local import search, translator
from mopidy.local import search, storage, translator
from mopidy.utils import encoding
logger = logging.getLogger(__name__)
# TODO: move to load and dump in models?
def load_library(json_file):
if not os.path.isfile(json_file):
logger.info(
'No local library metadata cache found at %s. Please run '
'`mopidy local scan` to index your local music library. '
'If you do not have a local music collection, you can disable the '
'local backend to hide this message.',
json_file)
return {}
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)
except (IOError, ValueError) as error:
logger.warning(
'Loading JSON local library failed: %s',
encoding.locale_decode(error))
return {}
@ -121,6 +132,8 @@ class JsonLibrary(local.Library):
self._json_file = os.path.join(
config['local']['data_dir'], b'library.json.gz')
storage.check_dirs_and_files(config)
def browse(self, uri):
if not self._browse_cache:
return []

View File

@ -3,8 +3,8 @@ from __future__ import unicode_literals
import logging
from mopidy import backend
from mopidy.local import translator
from . import translator
logger = logging.getLogger(__name__)

30
mopidy/local/storage.py Normal file
View File

@ -0,0 +1,30 @@
from __future__ import unicode_literals
import logging
import os
from mopidy.utils import encoding, path
logger = logging.getLogger(__name__)
def check_dirs_and_files(config):
if not os.path.isdir(config['local']['media_dir']):
logger.warning(
'Local media dir %s does not exist.' %
config['local']['media_dir'])
try:
path.get_or_create_dir(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(config['local']['playlists_dir'])
except EnvironmentError as error:
logger.warning(
'Could not create local playlists dir: %s',
encoding.locale_decode(error))

Some files were not shown because too many files have changed in this diff Show More