Release v0.14.0
This commit is contained in:
commit
027b0e2e8c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
*.swp
|
||||
.coverage
|
||||
|
||||
1
.mailmap
1
.mailmap
@ -1,5 +1,6 @@
|
||||
Thomas Adamcik <thomas@adamcik.no> <adamcik@samfundet.no>
|
||||
Thomas Adamcik <thomas@adamcik.no> <thomas+github@adamcik.no>
|
||||
Thomas Adamcik <thomas@adamcik.no> Thomas Adacmik <thomas@adamcik.no>
|
||||
Kristian Klette <klette@samfundet.no>
|
||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
|
||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@ -18,3 +18,4 @@
|
||||
- herrernst <herr.ernst@gmail.com>
|
||||
- Nick Steel <kingosticks@gmail.com>
|
||||
- Zan Dobersek <zandobersek@gmail.com>
|
||||
- Thomas Refis <refis.thomas@gmail.com>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
include *.ini
|
||||
include *.rst
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include data/mopidy.desktop
|
||||
include mopidy/backends/spotify/spotify_appkey.key
|
||||
include pylintrc
|
||||
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
recursive-include mopidy/frontends/http/data/
|
||||
|
||||
recursive-include mopidy *.conf
|
||||
recursive-include mopidy/frontends/http/data *
|
||||
recursive-include requirements *
|
||||
recursive-include tests *.py
|
||||
recursive-include tests/data *
|
||||
|
||||
32
README.rst
32
README.rst
@ -2,25 +2,27 @@
|
||||
Mopidy
|
||||
******
|
||||
|
||||
.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
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.
|
||||
|
||||
Mopidy is a music server which can play music both from your local hard drive
|
||||
and from Spotify. Searches returns results from both your local hard drive and
|
||||
from Spotify, and you can mix tracks from both sources in your play queue. Your
|
||||
Spotify playlists are also available for use, though we don't support modifying
|
||||
them yet.
|
||||
|
||||
To control your music server, you can use the Ubuntu Sound Menu on the machine
|
||||
running Mopidy, any device on the same network which can control UPnP
|
||||
MediaRenderers, or any MPD client. MPD clients are available for most
|
||||
platforms, including Windows, Mac OS X, Linux, Android and iOS.
|
||||
To control your Mopidy music server, you can use one of Mopidy's web clients,
|
||||
the Ubuntu Sound Menu, any device on the same network which can control UPnP
|
||||
MediaRenderers, or any MPD client. MPD clients are available for many
|
||||
platforms, including Windows, OS X, Linux, Android and iOS.
|
||||
|
||||
To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
|
||||
- `Documentation <http://docs.mopidy.com/>`_
|
||||
- `Source code <http://github.com/mopidy/mopidy>`_
|
||||
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
|
||||
- `CI server <http://travis-ci.org/mopidy/mopidy>`_
|
||||
- `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>`_
|
||||
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
- `Download development snapshot <http://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
|
||||
|
||||
.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
if __name__ == '__main__':
|
||||
from mopidy.__main__ import main
|
||||
main()
|
||||
@ -1,5 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
if __name__ == '__main__':
|
||||
from mopidy.scanner import main
|
||||
main()
|
||||
@ -12,6 +12,13 @@ backend. If you are working on a frontend and need to access the backend, see
|
||||
the :ref:`core-api`.
|
||||
|
||||
|
||||
Backend class
|
||||
=============
|
||||
|
||||
.. autoclass:: mopidy.backends.base.Backend
|
||||
:members:
|
||||
|
||||
|
||||
Playback provider
|
||||
=================
|
||||
|
||||
|
||||
33
docs/api/config.rst
Normal file
33
docs/api/config.rst
Normal file
@ -0,0 +1,33 @@
|
||||
.. _config-api:
|
||||
|
||||
**********
|
||||
Config API
|
||||
**********
|
||||
|
||||
.. automodule:: mopidy.config
|
||||
:synopsis: Config API for config loading and validation
|
||||
:members:
|
||||
|
||||
|
||||
Config section schemas
|
||||
======================
|
||||
|
||||
.. automodule:: mopidy.config.schemas
|
||||
:synopsis: Config section validation schemas
|
||||
:members:
|
||||
|
||||
|
||||
Config value types
|
||||
==================
|
||||
|
||||
.. automodule:: mopidy.config.types
|
||||
:synopsis: Config value validation types
|
||||
:members:
|
||||
|
||||
|
||||
Config value validators
|
||||
=======================
|
||||
|
||||
.. automodule:: mopidy.config.validators
|
||||
:synopsis: Config value validators
|
||||
:members:
|
||||
11
docs/api/ext.rst
Normal file
11
docs/api/ext.rst
Normal file
@ -0,0 +1,11 @@
|
||||
.. _ext-api:
|
||||
|
||||
*************
|
||||
Extension API
|
||||
*************
|
||||
|
||||
If you want to learn how to make Mopidy extensions, read :ref:`extensiondev`.
|
||||
|
||||
.. automodule:: mopidy.ext
|
||||
:synopsis: Extension API for extending Mopidy
|
||||
:members:
|
||||
@ -13,15 +13,18 @@ The following requirements applies to any frontend implementation:
|
||||
<http://pykka.readthedocs.org/>`_ actor, called the "main actor" from here
|
||||
on.
|
||||
|
||||
- The main actor MUST accept a constructor argument ``core``, which will be an
|
||||
:class:`ActorProxy <pykka.proxy.ActorProxy>` for the core actor. This object
|
||||
gives access to the full :ref:`core-api`.
|
||||
- The main actor MUST accept two constructor arguments:
|
||||
|
||||
- ``config``, which is a dict structure with the entire Mopidy configuration.
|
||||
|
||||
- ``core``, which will be an :class:`ActorProxy <pykka.proxy.ActorProxy>` for
|
||||
the core actor. This object gives access to the full :ref:`core-api`.
|
||||
|
||||
- It MAY use additional actors to implement whatever it does, and using actors
|
||||
in frontend implementations is encouraged.
|
||||
|
||||
- The frontend is activated by including its main actor in the
|
||||
:attr:`mopidy.settings.FRONTENDS` setting.
|
||||
- The frontend is enabled if the extension it is part of is enabled. See
|
||||
:ref:`extensiondev` for more information.
|
||||
|
||||
- The main actor MUST be able to start and stop the frontend when the main
|
||||
actor is started and stopped.
|
||||
@ -45,6 +48,6 @@ Frontend implementations
|
||||
========================
|
||||
|
||||
* :mod:`mopidy.frontends.http`
|
||||
* :mod:`mopidy.frontends.lastfm`
|
||||
* :mod:`mopidy.frontends.mpd`
|
||||
* :mod:`mopidy.frontends.mpris`
|
||||
* :mod:`mopidy.frontends.scrobbler`
|
||||
|
||||
439
docs/api/http.rst
Normal file
439
docs/api/http.rst
Normal file
@ -0,0 +1,439 @@
|
||||
.. _http-api:
|
||||
|
||||
********
|
||||
HTTP 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.
|
||||
|
||||
.. 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
|
||||
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.
|
||||
|
||||
|
||||
.. _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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Event messages
|
||||
--------------
|
||||
|
||||
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 can be recognized by checking for the key named
|
||||
``jsonrpc`` with the string value ``2.0``. For details on the messaging format,
|
||||
please refer to the `JSON-RPC 2.0 spec
|
||||
<http://www.jsonrpc.org/specification>`_.
|
||||
|
||||
All methods (not attributes) in the :ref:`core-api` is made available through
|
||||
JSON-RPC calls over the WebSocket. For example,
|
||||
:meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method
|
||||
``core.playback.play``.
|
||||
|
||||
The core API's attributes is made available through setters and getters. For
|
||||
example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is
|
||||
available as the JSON-RPC method ``core.playback.get_current_track``.
|
||||
|
||||
Example JSON-RPC request::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_current_track"}
|
||||
|
||||
Example JSON-RPC response::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", "...": "..."}}
|
||||
|
||||
The JSON-RPC method ``core.describe`` returns a data structure describing all
|
||||
available methods. If you're unsure how the core API maps to JSON-RPC, having a
|
||||
look at the ``core.describe`` response can be helpful.
|
||||
|
||||
|
||||
.. _mopidy-js:
|
||||
|
||||
Mopidy.js JavaScript library
|
||||
============================
|
||||
|
||||
We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets
|
||||
you quickly started with working on your client instead of figuring out how to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting the library 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/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/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").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.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
.. _api-ref:
|
||||
|
||||
*************
|
||||
API reference
|
||||
*************
|
||||
@ -11,3 +13,6 @@ API reference
|
||||
core
|
||||
audio
|
||||
frontends
|
||||
ext
|
||||
config
|
||||
http
|
||||
|
||||
@ -7,6 +7,21 @@ backends and between the backends and the MPD frontend. All fields are optional
|
||||
and immutable. In other words, they can only be set through the class
|
||||
constructor during instance creation.
|
||||
|
||||
If you want to modify a model, use the
|
||||
:meth:`~mopidy.models.ImmutableObject.copy` method. It accepts keyword
|
||||
arguments for the parts of the model you want to change, and copies the rest of
|
||||
the data from the model you call it on. Example::
|
||||
|
||||
>>> from mopidy.models import Track
|
||||
>>> track1 = Track(name='Christmas Carol', length=171)
|
||||
>>> track1
|
||||
Track(artists=[], length=171, name='Christmas Carol')
|
||||
>>> track2 = track1.copy(length=37)
|
||||
>>> track2
|
||||
Track(artists=[], length=37, name='Christmas Carol')
|
||||
>>> track1
|
||||
Track(artists=[], length=171, name='Christmas Carol')
|
||||
|
||||
|
||||
Data model relations
|
||||
====================
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
.. _authors:
|
||||
|
||||
*******
|
||||
Authors
|
||||
*******
|
||||
@ -6,11 +8,7 @@ Contributors to Mopidy in the order of appearance:
|
||||
|
||||
.. include:: ../AUTHORS
|
||||
|
||||
|
||||
Showing your appreciation
|
||||
=========================
|
||||
|
||||
If you already enjoy Mopidy, or don't enjoy it and want to help us making
|
||||
Mopidy better, the best way to do so is to contribute back to the community.
|
||||
You can contribute code, documentation, tests, bug reports, or help other
|
||||
users, spreading the word, etc.
|
||||
users, spreading the word, etc. See :ref:`contributing` for a head start.
|
||||
|
||||
@ -1,8 +1,115 @@
|
||||
*******
|
||||
Changes
|
||||
*******
|
||||
*********
|
||||
Changelog
|
||||
*********
|
||||
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
This changelog is used to track all major changes to Mopidy.
|
||||
|
||||
v0.14.0 (2013-04-28)
|
||||
====================
|
||||
|
||||
The 0.14 release has a clear focus on two things: the new configuration system
|
||||
and extension support. Mopidy's documentation has also been greatly extended
|
||||
and improved.
|
||||
|
||||
Since the last release a month ago, we've closed or merged 53 issues and pull
|
||||
requests. A total of seven :ref:`authors <authors>` have contributed, including
|
||||
one new.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- setuptools or distribute is now required. We've introduced this dependency to
|
||||
use setuptools' entry points functionality to find installed Mopidy
|
||||
extensions.
|
||||
|
||||
**New configuration system**
|
||||
|
||||
- Mopidy has a new configuration system based on ini-style files instead of a
|
||||
Python file. This makes configuration easier for users, and also makes it
|
||||
possible for Mopidy extensions to have their own config sections.
|
||||
|
||||
As part of this change we have cleaned up the naming of our config values.
|
||||
|
||||
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.
|
||||
|
||||
- A long wanted feature: You can now enable or disable specific frontends or
|
||||
backends without having to redefine :attr:`~mopidy.settings.FRONTENDS` or
|
||||
:attr:`~mopidy.settings.BACKENDS` in your config. Those config values are
|
||||
gone completely.
|
||||
|
||||
**Extension support**
|
||||
|
||||
- Mopidy now supports extensions. This means that any developer now easily can
|
||||
create a Mopidy extension to add new control interfaces or music backends.
|
||||
This helps spread the maintenance burden across more developers, and also
|
||||
makes it possible to extend Mopidy with new backends the core developers are
|
||||
unable to create and/or maintain because of geo restrictions, etc. If you're
|
||||
interested in creating an extension for Mopidy, read up on
|
||||
:ref:`extensiondev`.
|
||||
|
||||
- All of Mopidy's existing frontends and backends are now plugged into Mopidy
|
||||
as extensions, but they are still distributed together with Mopidy and are
|
||||
enabled by default.
|
||||
|
||||
- The NAD mixer have been moved out of Mopidy core to its own project,
|
||||
Mopidy-NAD. See :ref:`ext` for more information.
|
||||
|
||||
- Janez Troha has made the first two external extensions for Mopidy: a backend
|
||||
for playing music from Soundcloud, and a backend for playing music from a
|
||||
Beets music library. See :ref:`ext` for more information.
|
||||
|
||||
**Command line options**
|
||||
|
||||
- The command option :option:`mopidy --list-settings` is now named
|
||||
:option:`mopidy --show-config`.
|
||||
|
||||
- The command option :option:`mopidy --list-deps` is now named
|
||||
:option:`mopidy --show-deps`.
|
||||
|
||||
- What configuration files to use can now be specified through the command
|
||||
option :option:`mopidy --config`, multiple files can be specified using colon
|
||||
as a separator.
|
||||
|
||||
- Configuration values can now be overridden through the command option
|
||||
:option:`mopidy --option`. For example: ``mopidy --option
|
||||
spotify/enabled=false``.
|
||||
|
||||
- The GStreamer command line options, :option:`mopidy --gst-*` and
|
||||
:option:`mopidy --help-gst` are no longer supported. To set GStreamer debug
|
||||
flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer
|
||||
to GStreamer's documentation for details.
|
||||
|
||||
**Spotify backend**
|
||||
|
||||
- Add support for starred playlists, both your own and those owned by other
|
||||
users. (Fixes: :issue:`326`)
|
||||
|
||||
- Fix crash when a new playlist is added by another Spotify client. (Fixes:
|
||||
:issue:`387`, :issue:`425`)
|
||||
|
||||
**MPD frontend**
|
||||
|
||||
- Playlists with identical names are now handled properly by the MPD frontend
|
||||
by suffixing the duplicate names with e.g. ``[2]``. This is needed because
|
||||
MPD identify playlists by name only, while Mopidy and Spotify supports
|
||||
multiple playlists with the same name, and identify them using an URI.
|
||||
(Fixes: :issue:`114`)
|
||||
|
||||
**MPRIS frontend**
|
||||
|
||||
- The frontend is now disabled if the :envvar:`DISPLAY` environment variable is
|
||||
unset. This avoids some harmless error messages, that have been known to
|
||||
confuse new users debugging other problems.
|
||||
|
||||
**Development**
|
||||
|
||||
- Developers running Mopidy from a Git clone now need to run ``python setup.py
|
||||
develop`` to register the bundled extensions. If you don't do this, Mopidy
|
||||
will not find any frontends or backends. Note that we highly recomend you do
|
||||
this in a virtualenv, not system wide. As a bonus, the command also gives
|
||||
you a ``mopidy`` executable in your search path.
|
||||
|
||||
|
||||
v0.13.0 (2013-03-31)
|
||||
@ -311,7 +418,7 @@ We've added an HTTP frontend for those wanting to build web clients for Mopidy!
|
||||
**HTTP frontend**
|
||||
|
||||
- Added new optional HTTP frontend which exposes Mopidy's core API through
|
||||
JSON-RPC 2.0 messages over a WebSocket. See :ref:`http-frontend` for further
|
||||
JSON-RPC 2.0 messages over a WebSocket. See :ref:`http-api` for further
|
||||
details.
|
||||
|
||||
- Added a JavaScript library, Mopidy.js, to make it easier to develop web based
|
||||
@ -1471,8 +1578,7 @@ In the last two months, Mopidy's MPD frontend has gotten lots of stability
|
||||
fixes and error handling improvements, proper support for having the same track
|
||||
multiple times in a playlist, and support for IPv6. We have also fixed the
|
||||
choppy playback on the libspotify backend. For the road ahead of us, we got an
|
||||
updated :doc:`release roadmap <development>` with our goals for the 0.1 to 0.3
|
||||
releases.
|
||||
updated release roadmap with our goals for the 0.1 to 0.3 releases.
|
||||
|
||||
Enjoy the best alpha relase of Mopidy ever :-)
|
||||
|
||||
@ -4,14 +4,15 @@
|
||||
HTTP clients
|
||||
************
|
||||
|
||||
Mopidy added an :ref:`HTTP frontend <http-frontend>` in 0.10 which provides the
|
||||
building blocks needed for creating web clients for Mopidy with the help of a
|
||||
WebSocket and a JavaScript library provided by Mopidy.
|
||||
Mopidy added an :ref:`HTTP frontend <ext-http>` and an :ref:`HTTP API
|
||||
<http-api>` in 0.10 which together provides the building blocks needed for
|
||||
creating web clients for Mopidy with the help of a WebSocket and a JavaScript
|
||||
library provided by Mopidy.
|
||||
|
||||
This page will list any HTTP/web Mopidy clients. If you've created one, please
|
||||
notify us so we can include your client on this page.
|
||||
This page will list any Mopidy web clients using the HTTP frontend. If you've
|
||||
created one, please notify us so we can include your client on this page.
|
||||
|
||||
See :ref:`http-frontend` for details on how to build your own web client.
|
||||
See :ref:`http-api` for details on how to build your own web client.
|
||||
|
||||
|
||||
woutervanwijk/Mopidy-Webclient
|
||||
@ -24,9 +25,9 @@ woutervanwijk/Mopidy-Webclient
|
||||
The first web client for Mopidy is still under development, but is already very
|
||||
usable. It targets both desktop and mobile browsers.
|
||||
|
||||
To try it out, get a copy of https://github.com/woutervanwijk/Mopidy-WebClient
|
||||
and point the :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` setting towards
|
||||
your copy of the web client.
|
||||
The web client used for the `Pi Musicbox
|
||||
<http://www.woutervanwijk.nl/pimusicbox/>`_ is also available for other users
|
||||
of Mopidy. See https://github.com/woutervanwijk/Mopidy-WebClient for details.
|
||||
|
||||
|
||||
Mopidy Lux
|
||||
@ -47,13 +48,8 @@ New web client developed by Meantime IT in the UK for their office jukebox. See
|
||||
https://github.com/meantimeit/jukepi for details.
|
||||
|
||||
|
||||
Rompr
|
||||
=====
|
||||
Other web clients
|
||||
=================
|
||||
|
||||
.. image:: /_static/rompr.png
|
||||
:width: 557
|
||||
:height: 600
|
||||
|
||||
`Rompr <http://sourceforge.net/projects/rompr/>`_ is a web based MPD client.
|
||||
`mrvanes <https://github.com/mrvanes>`_, a Mopidy and Rompr user, said: "These
|
||||
projects are a real match made in heaven."
|
||||
For Mopidy web clients using Mopidy's MPD frontend instead of HTTP, see
|
||||
:ref:`mpd-web-clients`.
|
||||
|
||||
@ -320,7 +320,29 @@ purchased from `MPaD at iTunes Store
|
||||
when waiting for the connection to a server to succeed.
|
||||
|
||||
|
||||
.. _mpd-web-clients:
|
||||
|
||||
Web clients
|
||||
===========
|
||||
|
||||
See :ref:`http-clients`.
|
||||
The following web clients use the MPD protocol to communicate with Mopidy. For
|
||||
other web clients, see :ref:`http-clients`.
|
||||
|
||||
|
||||
Rompr
|
||||
-----
|
||||
|
||||
.. image:: /_static/rompr.png
|
||||
:width: 557
|
||||
:height: 600
|
||||
|
||||
`Rompr <http://sourceforge.net/projects/rompr/>`_ is a web based MPD client.
|
||||
`mrvanes <https://github.com/mrvanes>`_, a Mopidy and Rompr user, said: "These
|
||||
projects are a real match made in heaven."
|
||||
|
||||
|
||||
Partify
|
||||
-------
|
||||
|
||||
`Partify <http://www.partify.us/>`_ is a web based MPD client focusing on
|
||||
making music playing collaborative and social.
|
||||
|
||||
@ -8,9 +8,9 @@ MPRIS clients
|
||||
Specification. It's a spec that describes a standard D-Bus interface for making
|
||||
media players available to other applications on the same system.
|
||||
|
||||
Mopidy's :ref:`MPRIS frontend <mpris-frontend>` currently implements all
|
||||
required parts of the MPRIS spec, plus the optional playlist interface. It does
|
||||
not implement the optional tracklist interface.
|
||||
Mopidy's :ref:`MPRIS frontend <ext-mpris>` currently implements all required
|
||||
parts of the MPRIS spec, plus the optional playlist interface. It does not
|
||||
implement the optional tracklist interface.
|
||||
|
||||
|
||||
.. _ubuntu-sound-menu:
|
||||
@ -36,12 +36,11 @@ Mopidy executable. If this isn't in place, the sound menu will not detect that
|
||||
Mopidy is running.
|
||||
|
||||
Next, Mopidy's MPRIS frontend must be running for the sound menu to be able to
|
||||
control Mopidy. The frontend is activated by default, so unless you've changed
|
||||
the :attr:`mopidy.settings.FRONTENDS` setting, you should be good to go. Keep
|
||||
an eye out for warnings or errors from the MPRIS frontend when you start
|
||||
Mopidy, since it may fail because of missing dependencies or because Mopidy is
|
||||
started outside of X; the frontend won't work if ``$DISPLAY`` isn't set when
|
||||
Mopidy is started.
|
||||
control Mopidy. The frontend is enabled by default, so as long as you have all
|
||||
its dependencies available, you should be good to go. Keep an eye out for
|
||||
warnings or errors from the MPRIS frontend when you start Mopidy, since it may
|
||||
fail because of missing dependencies or because Mopidy is started outside of X;
|
||||
the frontend won't work if ``$DISPLAY`` isn't set when Mopidy is started.
|
||||
|
||||
Under normal use, if Mopidy isn't running and you open the menu and click on
|
||||
"Mopidy Music Server", a terminal window will open and automatically start
|
||||
|
||||
@ -37,18 +37,18 @@ How to make Mopidy available as an UPnP MediaRenderer
|
||||
|
||||
With the help of `the Rygel project <https://live.gnome.org/Rygel>`_ Mopidy can
|
||||
be made available as an UPnP MediaRenderer. Rygel will interface with Mopidy's
|
||||
:ref:`MPRIS frontend <mpris-frontend>`, and make Mopidy available as a
|
||||
MediaRenderer on the local network. Since this depends on the MPRIS frontend,
|
||||
which again depends on D-Bus being available, this will only work on Linux, and
|
||||
not OS X. MPRIS/D-Bus is only available to other applications on the same host,
|
||||
so Rygel must be running on the same machine as Mopidy.
|
||||
:ref:`MPRIS frontend <ext-mpris>`, and make Mopidy available as a MediaRenderer
|
||||
on the local network. Since this depends on the MPRIS frontend, which again
|
||||
depends on D-Bus being available, this will only work on Linux, and not OS X.
|
||||
MPRIS/D-Bus is only available to other applications on the same host, so Rygel
|
||||
must be running on the same machine as Mopidy.
|
||||
|
||||
1. Start Mopidy and make sure the :ref:`MPRIS frontend <mpris-frontend>` is
|
||||
working. It is activated by default, but you may miss dependencies or be
|
||||
using OS X, in which case it will not work. Check the console output when
|
||||
Mopidy is started for any errors related to the MPRIS frontend. If you're
|
||||
unsure it is working, there are instructions for how to test it on the
|
||||
:ref:`MPRIS frontend <mpris-frontend>` page.
|
||||
1. Start Mopidy and make sure the :ref:`MPRIS frontend <ext-mpris>` is working.
|
||||
It is activated by default, but you may miss dependencies or be using OS X,
|
||||
in which case it will not work. Check the console output when Mopidy is
|
||||
started for any errors related to the MPRIS frontend. If you're unsure it is
|
||||
working, there are instructions for how to test it on the :ref:`MPRIS
|
||||
frontend <ext-mpris>` page.
|
||||
|
||||
2. Install Rygel. On Debian/Ubuntu::
|
||||
|
||||
@ -66,11 +66,10 @@ so Rygel must be running on the same machine as Mopidy.
|
||||
|
||||
$ rygel
|
||||
Rygel-Message: New plugin 'MediaExport' available
|
||||
Rygel-Message: New plugin 'org.mpris.MediaPlayer2.spotify' available
|
||||
Rygel-Message: New plugin 'org.mpris.MediaPlayer2.mopidy' available
|
||||
|
||||
Note that in the above example, both the official Spotify client and Mopidy
|
||||
is running and made available through Rygel.
|
||||
In the above example, you can see that Rygel found Mopidy, and it is now
|
||||
making Mopidy available through Rygel.
|
||||
|
||||
|
||||
The UPnP-Inspector client
|
||||
|
||||
65
docs/codestyle.rst
Normal file
65
docs/codestyle.rst
Normal file
@ -0,0 +1,65 @@
|
||||
.. _codestyle:
|
||||
|
||||
**********
|
||||
Code style
|
||||
**********
|
||||
|
||||
- Always import ``unicode_literals`` and use unicode literals for everything
|
||||
except where you're explicitly working with bytes, which are marked with the
|
||||
``b`` prefix.
|
||||
|
||||
Do this::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
foo = 'I am a unicode string, which is a sane default'
|
||||
bar = b'I am a bytestring'
|
||||
|
||||
Not this::
|
||||
|
||||
foo = u'I am a unicode string'
|
||||
bar = 'I am a bytestring, but was it intentional?'
|
||||
|
||||
- Follow :pep:`8` unless otherwise noted. `flake8
|
||||
<http://pypi.python.org/pypi/flake8>`_ should be used to check your code
|
||||
against the guidelines.
|
||||
|
||||
- Use four spaces for indentation, *never* tabs.
|
||||
|
||||
- Use CamelCase with initial caps for class names::
|
||||
|
||||
ClassNameWithCamelCase
|
||||
|
||||
- Use underscore to split variable, function and method names for
|
||||
readability. Don't use CamelCase.
|
||||
|
||||
::
|
||||
|
||||
lower_case_with_underscores
|
||||
|
||||
- Use the fact that empty strings, lists and tuples are :class:`False` and
|
||||
don't compare boolean values using ``==`` and ``!=``.
|
||||
|
||||
- Follow whitespace rules as described in :pep:`8`. Good examples::
|
||||
|
||||
spam(ham[1], {eggs: 2})
|
||||
spam(1)
|
||||
dict['key'] = list[index]
|
||||
|
||||
- Limit lines to 80 characters and avoid trailing whitespace. However note that
|
||||
wrapped lines should be *one* indentation level in from level above, except
|
||||
for ``if``, ``for``, ``with``, and ``while`` lines which should have two
|
||||
levels of indentation::
|
||||
|
||||
if (foo and bar ...
|
||||
baz and foobar):
|
||||
a = 1
|
||||
|
||||
from foobar import (foo, bar, ...
|
||||
baz)
|
||||
|
||||
- For consistency, prefer ``'`` over ``"`` for strings, unless the string
|
||||
contains ``'``.
|
||||
|
||||
- Take a look at :pep:`20` for a nice peek into a general mindset useful for
|
||||
Python coding.
|
||||
15
docs/conf.py
15
docs/conf.py
@ -32,7 +32,8 @@ class Mock(object):
|
||||
def __getattr__(self, name):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'):
|
||||
elif (name[0] == name[0].upper()
|
||||
and not name.startswith('MIXER_TRACK_')):
|
||||
return type(name, (), {})
|
||||
else:
|
||||
return Mock()
|
||||
@ -53,7 +54,6 @@ MOCK_MODULES = [
|
||||
'pykka.future',
|
||||
'pykka.registry',
|
||||
'pylast',
|
||||
'serial',
|
||||
'ws4py',
|
||||
'ws4py.messaging',
|
||||
'ws4py.server',
|
||||
@ -98,7 +98,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Mopidy'
|
||||
copyright = '2010-2012, Stein Magnus Jodal and contributors'
|
||||
copyright = '2010-2013, Stein Magnus Jodal and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@ -266,3 +266,12 @@ latex_documents = [
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', '#')}
|
||||
|
||||
|
||||
def setup(app):
|
||||
from sphinx.ext.autodoc import cut_lines
|
||||
app.connect(b'autodoc-process-docstring', cut_lines(4, what=['module']))
|
||||
app.add_object_type(
|
||||
b'confval', 'confval',
|
||||
objname='configuration value',
|
||||
indextemplate='pair: %s; configuration value')
|
||||
|
||||
216
docs/config.rst
Normal file
216
docs/config.rst
Normal file
@ -0,0 +1,216 @@
|
||||
*************
|
||||
Configuration
|
||||
*************
|
||||
|
||||
Mopidy has a lot of config values you can tweak, but you only need to change a
|
||||
few to get up and running. A complete ``~/.config/mopidy/mopidy.conf`` may be
|
||||
as simple as this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[mpd]
|
||||
hostname = ::
|
||||
|
||||
[spotify]
|
||||
username = alice
|
||||
password = mysecret
|
||||
|
||||
Mopidy primarily reads config from the file ``~/.config/mopidy/mopidy.conf``,
|
||||
where ``~`` means your *home directory*. If your username is ``alice`` and you
|
||||
are running Linux, the settings file should probably be at
|
||||
``/home/alice/.config/mopidy/mopidy.conf``. You can either create the
|
||||
configuration file yourself, or run the ``mopidy`` command, and it will create
|
||||
an empty settings file for you and print what config values must be set
|
||||
to successfully start Mopidy.
|
||||
|
||||
When you have created the configuration file, open it in a text editor, and add
|
||||
the config values you want to change. If you want to keep the default for a
|
||||
config value, you **should not** add it to ``~/.config/mopidy/mopidy.conf``.
|
||||
|
||||
To see what's the effective configuration for your Mopidy installation, you can
|
||||
run :option:`mopidy --show-config`. It will print your full effective config
|
||||
with passwords masked out so that you safely can share the output with others
|
||||
for debugging.
|
||||
|
||||
You can find a description of all config values belonging to Mopidy's core
|
||||
below, together with their default values. In addition, all :ref:`extensions
|
||||
<ext>` got additional config values. The extension's config values and config
|
||||
defaults are documented on the :ref:`extension pages <ext>`.
|
||||
|
||||
|
||||
Default core configuration
|
||||
==========================
|
||||
|
||||
.. literalinclude:: ../mopidy/config/default.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Core configuration values
|
||||
=========================
|
||||
|
||||
.. confval:: audio/mixer
|
||||
|
||||
Audio mixer to use.
|
||||
|
||||
Expects a GStreamer mixer to use, typical values are: ``autoaudiomixer``,
|
||||
``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``.
|
||||
|
||||
The default is ``autoaudiomixer``, which attempts to select a sane 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. ``software``
|
||||
can be used to force software mixing in the application.
|
||||
|
||||
.. 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.
|
||||
|
||||
Expects a GStreamer sink. Typical values are ``autoaudiosink``,
|
||||
``alsasink``, ``osssink``, ``oss4sink``, ``pulsesink``, and ``shout2send``,
|
||||
and additional arguments specific to each sink. You can use the command
|
||||
``gst-inspect-0.10`` to see what output properties can be set on the sink.
|
||||
For example: ``gst-inspect-0.10 shout2send``
|
||||
|
||||
.. confval:: logging/console_format
|
||||
|
||||
The log format used for informational logging.
|
||||
|
||||
See `the Python logging docs`_ for details on the format.
|
||||
|
||||
.. confval:: logging/debug_format
|
||||
|
||||
The log format used for debug logging.
|
||||
|
||||
See `the Python logging docs`_ for details on the format.
|
||||
|
||||
.. confval:: logging/debug_file
|
||||
|
||||
The file to dump debug log data to when Mopidy is run with the
|
||||
:option:`mopidy --save-debug-log` option.
|
||||
|
||||
.. confval:: logging/config_file
|
||||
|
||||
Config file that overrides all logging settings, see `the Python logging
|
||||
docs`_ for details.
|
||||
|
||||
.. confval:: loglevels/*
|
||||
|
||||
The ``loglevels`` config section can be used to change the log level for
|
||||
specific parts of Mopidy during development or debugging. Each key in the
|
||||
config section should match the name of a logger. The value is the log
|
||||
level to use for that logger, one of ``debug``, ``info``, ``warning``,
|
||||
``error``, or ``critical``.
|
||||
|
||||
.. confval:: proxy/hostname
|
||||
|
||||
Proxy server to use for communication with the Internet.
|
||||
|
||||
Currently only used by the Spotify extension.
|
||||
|
||||
.. confval:: proxy/username
|
||||
|
||||
Username for the proxy server, if needed.
|
||||
|
||||
.. confval:: proxy/password
|
||||
|
||||
Password for the proxy server, if needed.
|
||||
|
||||
.. _the Python logging docs:
|
||||
|
||||
http://docs.python.org/2/library/logging.config.html
|
||||
|
||||
|
||||
Advanced configurations
|
||||
=======================
|
||||
|
||||
Custom audio sink
|
||||
-----------------
|
||||
|
||||
If you have successfully installed GStreamer, and then run the ``gst-inspect``
|
||||
or ``gst-inspect-0.10`` command, you should see a long listing of installed
|
||||
plugins, ending in a summary line::
|
||||
|
||||
$ gst-inspect-0.10
|
||||
... long list of installed plugins ...
|
||||
Total count: 254 plugins (1 blacklist entry not shown), 1156 features
|
||||
|
||||
Next, you should be able to produce a audible tone by running::
|
||||
|
||||
gst-launch-0.10 audiotestsrc ! audioresample ! autoaudiosink
|
||||
|
||||
If you cannot hear any sound when running this command, you won't hear any
|
||||
sound from Mopidy either, as Mopidy by default uses GStreamer's
|
||||
``autoaudiosink`` to play audio. Thus, make this work before you file a bug
|
||||
against Mopidy.
|
||||
|
||||
If you for some reason want to use some other GStreamer audio sink than
|
||||
``autoaudiosink``, you can set the :confval:`audio/output` config value to a
|
||||
partial GStreamer pipeline description describing the GStreamer sink you want
|
||||
to use.
|
||||
|
||||
Example ``mopidy.conf`` for using OSS4:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[audio]
|
||||
output = oss4sink
|
||||
|
||||
Again, this is the equivalent of the following ``gst-inspect`` command, so make
|
||||
this work first::
|
||||
|
||||
gst-launch-0.10 audiotestsrc ! audioresample ! oss4sink
|
||||
|
||||
|
||||
Streaming through SHOUTcast/Icecast
|
||||
-----------------------------------
|
||||
|
||||
If you want to play the audio on another computer than the one running Mopidy,
|
||||
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
|
||||
streaming server. Multiple media players can then be connected to the streaming
|
||||
server simultaneously. To use the SHOUTcast output, do the following:
|
||||
|
||||
#. Install, configure and start the Icecast server. It can be found in the
|
||||
``icecast2`` package in Debian/Ubuntu.
|
||||
|
||||
#. Set the :confval:`audio/output` config value to ``lame ! shout2send``. An
|
||||
Ogg Vorbis encoder could be used instead of the lame MP3 encoder.
|
||||
|
||||
#. 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, to set the username and password, use:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[audio]
|
||||
output = lame ! shout2send username="alice" password="secret"
|
||||
|
||||
Other advanced setups are also possible for outputs. Basically, anything you
|
||||
can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
:confval:`audio/output`.
|
||||
|
||||
|
||||
New configuration values
|
||||
------------------------
|
||||
|
||||
Mopidy's settings validator will stop you from defining any config values in
|
||||
your settings file that Mopidy doesn't know about. This may sound obnoxious,
|
||||
but it helps us detect typos in your settings, and deprecated settings that
|
||||
should be removed or updated.
|
||||
|
||||
If you're extending Mopidy, and want to use Mopidy's configuration
|
||||
system, you can add new sections to the config without triggering the config
|
||||
validator. We recommend that you choose a good and unique name for the config
|
||||
section so that multiple extensions to Mopidy can be used at the same time
|
||||
without any danger of naming collisions.
|
||||
139
docs/contributing.rst
Normal file
139
docs/contributing.rst
Normal file
@ -0,0 +1,139 @@
|
||||
.. _contributing:
|
||||
|
||||
************
|
||||
Contributing
|
||||
************
|
||||
|
||||
If you are thinking about making Mopidy better, or you just want to hack on it,
|
||||
that’s great. Here are some tips to get you started.
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
#. Make sure you have a `GitHub account <https://github.com/signup/free>`_.
|
||||
|
||||
#. `Submit <https://github.com/mopidy/mopidy/issues/new>`_ a ticket for your
|
||||
issue, assuming one does not already exist. Clearly describe the issue
|
||||
including steps to reproduce when it is a bug.
|
||||
|
||||
#. Fork the repository on GitHub.
|
||||
|
||||
|
||||
Making changes
|
||||
==============
|
||||
|
||||
#. Clone your fork on GitHub to your computer.
|
||||
|
||||
#. Consider making a Python `virtualenv <http://www.virtualenv.org/>`_ for
|
||||
Mopidy development to wall of Mopidy and it's dependencies from the rest of
|
||||
your system. If you do so, create the virtualenv with the
|
||||
``--system-site-packages`` flag so that Mopidy can use globally installed
|
||||
dependencies like GStreamer. If you don't use a virtualenv, you may need to
|
||||
run the following ``pip`` and ``python setup.py`` commands with ``sudo`` to
|
||||
install stuff globally on your computer.
|
||||
|
||||
#. Install dependencies as described in the :ref:`installation` section.
|
||||
|
||||
#. Checkout a new branch (usually based on ``develop``) and name it accordingly
|
||||
to what you intend to do.
|
||||
|
||||
- Features get the prefix ``feature/``
|
||||
|
||||
- Bug fixes get the prefix ``fix/``
|
||||
|
||||
- Improvements to the documentation get the prefix ``docs/``
|
||||
|
||||
|
||||
.. _run-from-git:
|
||||
|
||||
Running Mopidy from Git
|
||||
=======================
|
||||
|
||||
If you want to hack on Mopidy, you should run Mopidy directly from the Git
|
||||
repo.
|
||||
|
||||
#. Go to the Git repo root::
|
||||
|
||||
cd mopidy/
|
||||
|
||||
#. To get a ``mopidy`` executable and register all bundled extensions with
|
||||
setuptools, run::
|
||||
|
||||
python setup.py develop
|
||||
|
||||
It still works to run ``python mopidy`` directly on the ``mopidy`` Python
|
||||
package directory, but if you have never run ``python setup.py develop`` the
|
||||
extensions bundled with Mopidy isn't registered with setuptools, so Mopidy
|
||||
will start without any frontends or backends, making it quite useless.
|
||||
|
||||
#. Now you can run the Mopidy command, and it will run using the code
|
||||
in the Git repo::
|
||||
|
||||
mopidy
|
||||
|
||||
If you do any changes to the code, you'll just need to restart ``mopidy``
|
||||
to see the changes take effect.
|
||||
|
||||
|
||||
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 -r requirements/tests.txt
|
||||
|
||||
#. Then, 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::
|
||||
|
||||
nosetests --with-coverage tests/
|
||||
|
||||
#. Check the code for errors and style issues using flake8::
|
||||
|
||||
flake8 .
|
||||
|
||||
For more documentation on testing, check out the `nose documentation
|
||||
<http://nose.readthedocs.org/>`_.
|
||||
|
||||
|
||||
Submitting changes
|
||||
==================
|
||||
|
||||
- One branch per feature or fix. Keep branches small and on topic.
|
||||
|
||||
- Follow the :ref:`code style <codestyle>`, especially make sure ``flake8``
|
||||
does not complain about anything.
|
||||
|
||||
- Write good commit messages. Here's three blog posts on how to do it right:
|
||||
|
||||
- `Writing Git commit messages
|
||||
<http://365git.tumblr.com/post/3308646748/writing-git-commit-messages>`_
|
||||
|
||||
- `A Note About Git Commit Messages
|
||||
<http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_
|
||||
|
||||
- `On commit messages
|
||||
<http://who-t.blogspot.ch/2009/12/on-commit-messages.html>`_
|
||||
|
||||
- Send a pull request to the ``develop`` branch. See the `GitHub pull request
|
||||
docs <https://help.github.com/articles/using-pull-requests>`_ for help.
|
||||
|
||||
|
||||
Additional resources
|
||||
====================
|
||||
|
||||
- IRC channel: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||
|
||||
- `Mailing List <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
|
||||
- `GitHub documentation <https://help.github.com/>`_
|
||||
@ -1,365 +0,0 @@
|
||||
***********
|
||||
Development
|
||||
***********
|
||||
|
||||
Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at
|
||||
``irc.freenode.net`` and through `GitHub <https://github.com/>`_.
|
||||
|
||||
|
||||
Release schedule
|
||||
================
|
||||
|
||||
We intend to have about one timeboxed feature release every month
|
||||
in periods of active development. The feature releases are numbered 0.x.0. The
|
||||
features added is a mix of what we feel is most important/requested of the
|
||||
missing features, and features we develop just because we find them fun to
|
||||
make, even though they may be useful for very few users or for a limited use
|
||||
case.
|
||||
|
||||
Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs
|
||||
that are too serious to wait for the next feature release. We will only release
|
||||
bugfix releases for the last feature release. E.g. when 0.3.0 is released, we
|
||||
will no longer provide bugfix releases for the 0.2 series. In other words,
|
||||
there will be just a single supported release at any point in time.
|
||||
|
||||
|
||||
Feature wishlist
|
||||
================
|
||||
|
||||
We maintain our collection of sane or less sane ideas for future Mopidy
|
||||
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
|
||||
labeled with `the "wishlist" label
|
||||
<https://github.com/mopidy/mopidy/issues?labels=wishlist>`_. Feel free to vote
|
||||
up any feature you would love to see in Mopidy, but please refrain from adding
|
||||
a comment just to say "I want this too!". You are of course free to add
|
||||
comments if you have suggestions for how the feature should work or be
|
||||
implemented, and you may add new wishlist issues if your ideas are not already
|
||||
represented.
|
||||
|
||||
|
||||
.. _run-from-git:
|
||||
|
||||
Run Mopidy from Git repo
|
||||
========================
|
||||
|
||||
If you want to contribute to the development of Mopidy, you should run Mopidy
|
||||
directly from the Git repo.
|
||||
|
||||
#. First of all, install Mopidy in the recommended way for your OS and/or
|
||||
distribution, like described at :ref:`installation`. You can have a
|
||||
system-wide installation of the last Mopidy release in addition to the Git
|
||||
repo which you run from when you code on Mopidy.
|
||||
|
||||
#. Then install Git, if haven't already. For Ubuntu/Debian::
|
||||
|
||||
sudo apt-get install git-core
|
||||
|
||||
On OS X using Homebrew::
|
||||
|
||||
sudo brew install git
|
||||
|
||||
#. Clone the official Mopidy repository::
|
||||
|
||||
git clone git://github.com/mopidy/mopidy.git
|
||||
|
||||
or your own fork of it::
|
||||
|
||||
git clone git@github.com:mygithubuser/mopidy.git
|
||||
|
||||
#. You can then run Mopidy directly from the Git repository::
|
||||
|
||||
cd mopidy/ # Move into the Git repo dir
|
||||
python mopidy # Run python on the mopidy source code dir
|
||||
|
||||
How you update your clone depends on whether you cloned the official Mopidy
|
||||
repository or your own fork, whether you have made any changes to the clone
|
||||
or not, and whether you are currently working on a feature branch or not. In
|
||||
other words, you'll need to learn Git.
|
||||
|
||||
For an introduction to Git, please visit `git-scm.com <http://git-scm.com/>`_.
|
||||
Also, please read the rest of our developer documentation before you start
|
||||
contributing.
|
||||
|
||||
|
||||
Code style
|
||||
==========
|
||||
|
||||
- Always import ``unicode_literals`` and use unicode literals for everything
|
||||
except where you're explicitly working with bytes, which are marked with the
|
||||
``b`` prefix.
|
||||
|
||||
Do this::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
foo = 'I am a unicode string, which is a sane default'
|
||||
bar = b'I am a bytestring'
|
||||
|
||||
Not this::
|
||||
|
||||
foo = u'I am a unicode string'
|
||||
bar = 'I am a bytestring, but was it intentional?'
|
||||
|
||||
- Follow :pep:`8` unless otherwise noted. `pep8.py
|
||||
<http://pypi.python.org/pypi/pep8/>`_ or `flake8
|
||||
<http://pypi.python.org/pypi/flake8>`_ can be used to check your code
|
||||
against the guidelines, however remember that matching the style of the
|
||||
surrounding code is also important.
|
||||
|
||||
- Use four spaces for indentation, *never* tabs.
|
||||
|
||||
- Use CamelCase with initial caps for class names::
|
||||
|
||||
ClassNameWithCamelCase
|
||||
|
||||
- Use underscore to split variable, function and method names for
|
||||
readability. Don't use CamelCase.
|
||||
|
||||
::
|
||||
|
||||
lower_case_with_underscores
|
||||
|
||||
- Use the fact that empty strings, lists and tuples are :class:`False` and
|
||||
don't compare boolean values using ``==`` and ``!=``.
|
||||
|
||||
- Follow whitespace rules as described in :pep:`8`. Good examples::
|
||||
|
||||
spam(ham[1], {eggs: 2})
|
||||
spam(1)
|
||||
dict['key'] = list[index]
|
||||
|
||||
- Limit lines to 80 characters and avoid trailing whitespace. However note that
|
||||
wrapped lines should be *one* indentation level in from level above, except
|
||||
for ``if``, ``for``, ``with``, and ``while`` lines which should have two
|
||||
levels of indentation::
|
||||
|
||||
if (foo and bar ...
|
||||
baz and foobar):
|
||||
a = 1
|
||||
|
||||
from foobar import (foo, bar, ...
|
||||
baz)
|
||||
|
||||
- For consistency, prefer ``'`` over ``"`` for strings, unless the string
|
||||
contains ``'``.
|
||||
|
||||
- Take a look at :pep:`20` for a nice peek into a general mindset useful for
|
||||
Python coding.
|
||||
|
||||
|
||||
Commit guidelines
|
||||
=================
|
||||
|
||||
- We follow the development process described at
|
||||
`nvie.com <http://nvie.com/posts/a-successful-git-branching-model/>`_.
|
||||
|
||||
- Keep commits small and on topic.
|
||||
|
||||
- If a commit looks too big you should be working in a feature branch not a
|
||||
single commit.
|
||||
|
||||
- Merge feature branches with ``--no-ff`` to keep track of the merge.
|
||||
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
To run tests, you need a couple of dependencies. They can be installed through
|
||||
Debian/Ubuntu package management::
|
||||
|
||||
sudo apt-get install python-coverage python-mock python-nose
|
||||
|
||||
Or, they can be installed using ``pip``::
|
||||
|
||||
sudo pip install -r requirements/tests.txt
|
||||
|
||||
Then, to run all tests, go to the project directory and run::
|
||||
|
||||
nosetests
|
||||
|
||||
For example::
|
||||
|
||||
$ nosetests
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................................
|
||||
.............................................................
|
||||
-----------------------------------------------------------------------------
|
||||
1062 tests run in 7.4 seconds (1062 tests passed)
|
||||
|
||||
To run tests with test coverage statistics, remember to specify the tests dir::
|
||||
|
||||
nosetests --with-coverage tests/
|
||||
|
||||
For more documentation on testing, check out the `nose documentation
|
||||
<http://nose.readthedocs.org/>`_.
|
||||
|
||||
|
||||
Continuous integration
|
||||
======================
|
||||
|
||||
Mopidy uses the free service `Travis CI <https://travis-ci.org/mopidy/mopidy>`_
|
||||
for automatically running the test suite when code is pushed to GitHub. This
|
||||
works both for the main Mopidy repo, but also for any forks. This way, any
|
||||
contributions to Mopidy through GitHub will automatically be tested by Travis
|
||||
CI, and the build status will be visible in the GitHub pull request interface,
|
||||
making it easier to evaluate the quality of pull requests.
|
||||
|
||||
In addition, we run a Jenkins CI server at http://ci.mopidy.com/ that runs all
|
||||
test on multiple platforms (Ubuntu, OS X, x86, arm) for every commit we push to
|
||||
the ``develop`` branch in the main Mopidy repo on GitHub. Thus, new code isn't
|
||||
tested by Jenkins before it is merged into the ``develop`` branch, which is a
|
||||
bit late, but good enough to get broad testing before new code is released.
|
||||
|
||||
In addition to running tests, the Jenkins CI server also gathers coverage
|
||||
statistics and uses pylint to check for errors and possible improvements in our
|
||||
code. So, if you're out of work, the code coverage and pylint data at the CI
|
||||
server should give you a place to start.
|
||||
|
||||
|
||||
Protocol debugging
|
||||
==================
|
||||
|
||||
Since the main interface provided to Mopidy is through the MPD protocol, it is
|
||||
crucial that we try and stay in sync with protocol developments. In an attempt
|
||||
to make it easier to debug differences Mopidy and MPD protocol handling we have
|
||||
created ``tools/debug-proxy.py``.
|
||||
|
||||
This tool is proxy that sits in front of two MPD protocol aware servers and
|
||||
sends all requests to both, returning the primary response to the client and
|
||||
then printing any diff in the two responses.
|
||||
|
||||
Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time
|
||||
of writing. See ``--help`` for available options. Sample session::
|
||||
|
||||
[127.0.0.1]:59714
|
||||
listallinfo
|
||||
--- Reference response
|
||||
+++ Actual response
|
||||
@@ -1,16 +1,1 @@
|
||||
-file: uri1
|
||||
-Time: 4
|
||||
-Artist: artist1
|
||||
-Title: track1
|
||||
-Album: album1
|
||||
-file: uri2
|
||||
-Time: 4
|
||||
-Artist: artist2
|
||||
-Title: track2
|
||||
-Album: album2
|
||||
-file: uri3
|
||||
-Time: 4
|
||||
-Artist: artist3
|
||||
-Title: track3
|
||||
-Album: album3
|
||||
-OK
|
||||
+ACK [2@0] {listallinfo} incorrect arguments
|
||||
|
||||
To ensure that Mopidy and MPD have comparable state it is suggested you setup
|
||||
both to use ``tests/data/advanced_tag_cache`` for their tag cache and
|
||||
``tests/data/scanner/advanced/`` for the music folder and ``tests/data`` for
|
||||
playlists.
|
||||
|
||||
|
||||
Setting profiles during development
|
||||
===================================
|
||||
|
||||
While developing Mopidy switching settings back and forth can become an all too
|
||||
frequent occurrence. As a quick hack to get around this you can structure your
|
||||
settings file in the following way::
|
||||
|
||||
import os
|
||||
profile = os.environ.get('PROFILE', '').split(',')
|
||||
|
||||
if 'spotify' in profile:
|
||||
BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
|
||||
elif 'local' in profile:
|
||||
BACKENDS = (u'mopidy.backends.local.LocalBackend',)
|
||||
LOCAL_MUSIC_PATH = u'~/music'
|
||||
|
||||
if 'shoutcast' in profile:
|
||||
OUTPUT = u'lame ! shout2send mount="/stream"'
|
||||
elif 'silent' in profile:
|
||||
OUTPUT = u'fakesink'
|
||||
MIXER = None
|
||||
|
||||
SPOTIFY_USERNAME = u'xxxxx'
|
||||
SPOTIFY_PASSWORD = u'xxxxx'
|
||||
|
||||
Using this setup you can now run Mopidy with ``PROFILE=silent,spotify mopidy``
|
||||
if you for instance want to test Spotify without any actual audio output.
|
||||
|
||||
|
||||
Debugging deadlocks
|
||||
===================
|
||||
|
||||
Between the numerous Pykka threads and GStreamer interactions there can
|
||||
sometimes be a potential for deadlocks. In an effort to make these slightly
|
||||
simpler to debug Mopidy registers a ``SIGUSR1`` signal handler which logs the
|
||||
traceback of all alive threads.
|
||||
|
||||
To trigger the signal handler, you can use the ``pkill`` command to
|
||||
send the ``SIGUSR1`` signal to any Mopidy processes::
|
||||
|
||||
pkill -SIGUSR1 mopidy
|
||||
|
||||
If you check the log, you should now find one log record with a full traceback
|
||||
for each of the currently alive threads in Mopidy.
|
||||
|
||||
|
||||
Writing documentation
|
||||
=====================
|
||||
|
||||
To write documentation, we use `Sphinx <http://sphinx-doc.org/>`_. See their
|
||||
site for lots of documentation on how to use Sphinx. To generate HTML or LaTeX
|
||||
from the documentation files, you need some additional dependencies.
|
||||
|
||||
You can install them through Debian/Ubuntu package management::
|
||||
|
||||
sudo apt-get install python-sphinx python-pygraphviz graphviz
|
||||
|
||||
Then, to generate docs::
|
||||
|
||||
cd docs/
|
||||
make # For help on available targets
|
||||
make html # To generate HTML docs
|
||||
|
||||
The documentation at http://docs.mopidy.com/ is automatically updated when a
|
||||
documentation update is pushed to ``mopidy/mopidy`` at GitHub.
|
||||
|
||||
|
||||
Creating releases
|
||||
=================
|
||||
|
||||
#. Update changelog and commit it.
|
||||
|
||||
#. Merge the release branch (``develop`` in the example) into master::
|
||||
|
||||
git checkout master
|
||||
git merge --no-ff -m "Release v0.2.0" develop
|
||||
|
||||
#. Tag the release::
|
||||
|
||||
git tag -a -m "Release v0.2.0" v0.2.0
|
||||
|
||||
#. Push to GitHub::
|
||||
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
#. Build package and upload to PyPI::
|
||||
|
||||
rm MANIFEST # Will be regenerated by setup.py
|
||||
python setup.py sdist upload
|
||||
|
||||
#. Spread the word.
|
||||
121
docs/devtools.rst
Normal file
121
docs/devtools.rst
Normal file
@ -0,0 +1,121 @@
|
||||
*****************
|
||||
Development tools
|
||||
*****************
|
||||
|
||||
Here you'll find description of the development tools we use.
|
||||
|
||||
|
||||
Continuous integration
|
||||
======================
|
||||
|
||||
Mopidy uses the free service `Travis CI <https://travis-ci.org/mopidy/mopidy>`_
|
||||
for automatically running the test suite when code is pushed to GitHub. This
|
||||
works both for the main Mopidy repo, but also for any forks. This way, any
|
||||
contributions to Mopidy through GitHub will automatically be tested by Travis
|
||||
CI, and the build status will be visible in the GitHub pull request interface,
|
||||
making it easier to evaluate the quality of pull requests.
|
||||
|
||||
In addition, we run a Jenkins CI server at http://ci.mopidy.com/ that runs all
|
||||
test on multiple platforms (Ubuntu, OS X, x86, arm) for every commit we push to
|
||||
the ``develop`` branch in the main Mopidy repo on GitHub. Thus, new code isn't
|
||||
tested by Jenkins before it is merged into the ``develop`` branch, which is a
|
||||
bit late, but good enough to get broad testing before new code is released.
|
||||
|
||||
In addition to running tests, the Jenkins CI server also gathers coverage
|
||||
statistics and uses pylint to check for errors and possible improvements in our
|
||||
code. So, if you're out of work, the code coverage and pylint data at the CI
|
||||
server should give you a place to start.
|
||||
|
||||
|
||||
Protocol debugger
|
||||
=================
|
||||
|
||||
Since the main interface provided to Mopidy is through the MPD protocol, it is
|
||||
crucial that we try and stay in sync with protocol developments. In an attempt
|
||||
to make it easier to debug differences Mopidy and MPD protocol handling we have
|
||||
created ``tools/debug-proxy.py``.
|
||||
|
||||
This tool is proxy that sits in front of two MPD protocol aware servers and
|
||||
sends all requests to both, returning the primary response to the client and
|
||||
then printing any diff in the two responses.
|
||||
|
||||
Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time
|
||||
of writing. See :option:`tools/debug-proxy.py --help` for available options.
|
||||
Sample session::
|
||||
|
||||
[127.0.0.1]:59714
|
||||
listallinfo
|
||||
--- Reference response
|
||||
+++ Actual response
|
||||
@@ -1,16 +1,1 @@
|
||||
-file: uri1
|
||||
-Time: 4
|
||||
-Artist: artist1
|
||||
-Title: track1
|
||||
-Album: album1
|
||||
-file: uri2
|
||||
-Time: 4
|
||||
-Artist: artist2
|
||||
-Title: track2
|
||||
-Album: album2
|
||||
-file: uri3
|
||||
-Time: 4
|
||||
-Artist: artist3
|
||||
-Title: track3
|
||||
-Album: album3
|
||||
-OK
|
||||
+ACK [2@0] {listallinfo} incorrect arguments
|
||||
|
||||
To ensure that Mopidy and MPD have comparable state it is suggested you setup
|
||||
both to use ``tests/data/advanced_tag_cache`` for their tag cache and
|
||||
``tests/data/scanner/advanced/`` for the music folder and ``tests/data`` for
|
||||
playlists.
|
||||
|
||||
|
||||
Documentation writing
|
||||
=====================
|
||||
|
||||
To write documentation, we use `Sphinx <http://sphinx-doc.org/>`_. See their
|
||||
site for lots of documentation on how to use Sphinx. To generate HTML from the
|
||||
documentation files, you need some additional dependencies.
|
||||
|
||||
You can install them through Debian/Ubuntu package management::
|
||||
|
||||
sudo apt-get install python-sphinx python-pygraphviz graphviz
|
||||
|
||||
Then, to generate docs::
|
||||
|
||||
cd docs/
|
||||
make # For help on available targets
|
||||
make html # To generate HTML docs
|
||||
|
||||
The documentation at http://docs.mopidy.com/ is automatically updated when a
|
||||
documentation update is pushed to ``mopidy/mopidy`` at GitHub.
|
||||
|
||||
|
||||
Creating releases
|
||||
=================
|
||||
|
||||
#. Update changelog and commit it.
|
||||
|
||||
#. Merge the release branch (``develop`` in the example) into master::
|
||||
|
||||
git checkout master
|
||||
git merge --no-ff -m "Release v0.2.0" develop
|
||||
|
||||
#. Tag the release::
|
||||
|
||||
git tag -a -m "Release v0.2.0" v0.2.0
|
||||
|
||||
#. Push to GitHub::
|
||||
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
#. Build package and upload to PyPI::
|
||||
|
||||
python setup.py sdist upload
|
||||
|
||||
#. Update the Debian package.
|
||||
|
||||
#. Spread the word.
|
||||
103
docs/ext/http.rst
Normal file
103
docs/ext/http.rst
Normal file
@ -0,0 +1,103 @@
|
||||
.. _ext-http:
|
||||
|
||||
***********
|
||||
Mopidy-HTTP
|
||||
***********
|
||||
|
||||
The HTTP extension lets you control Mopidy through HTTP and WebSockets, e.g.
|
||||
from a web based client. See :ref:`http-api` for details on how to integrate
|
||||
with Mopidy over HTTP.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=HTTP+frontend
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
.. literalinclude:: ../../requirements/http.txt
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/http/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: http/enabled
|
||||
|
||||
If the HTTP extension should be enabled or not.
|
||||
|
||||
.. confval:: http/hostname
|
||||
|
||||
Which address the HTTP server should bind to.
|
||||
|
||||
``127.0.0.1``
|
||||
Listens only on the IPv4 loopback interface
|
||||
``::1``
|
||||
Listens only on the IPv6 loopback interface
|
||||
``0.0.0.0``
|
||||
Listens on all IPv4 interfaces
|
||||
``::``
|
||||
Listens on all interfaces, both IPv4 and IPv6
|
||||
|
||||
.. confval:: http/port
|
||||
|
||||
Which TCP port the HTTP server should listen to.
|
||||
|
||||
.. confval:: http/static_dir
|
||||
|
||||
Which directory the HTTP server should serve at "/"
|
||||
|
||||
Change this to have Mopidy serve e.g. files for your JavaScript client.
|
||||
"/mopidy" will continue to work as usual even if you change this setting.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default if all dependencies are available.
|
||||
|
||||
When it is enabled it starts a web server at the port specified by the
|
||||
:confval:`http/port` config value.
|
||||
|
||||
.. warning:: Security
|
||||
|
||||
As a simple security measure, the web server is by default only available
|
||||
from localhost. To make it available from other computers, change the
|
||||
:confval:`http/hostname` config value. Before you do so, note that the HTTP
|
||||
extension does not feature any form of user authentication or
|
||||
authorization. Anyone able to access the web server can use the full core
|
||||
API of Mopidy. Thus, you probably only want to make the web server
|
||||
available from your local network or place it behind a web proxy which
|
||||
takes care or user authentication. You have been warned.
|
||||
|
||||
|
||||
Using a web based Mopidy client
|
||||
-------------------------------
|
||||
|
||||
The web server can also host any static files, for example the HTML, CSS,
|
||||
JavaScript, and images needed for a web based Mopidy client. To host static
|
||||
files, change the ``http/static_dir`` to point to the root directory of your
|
||||
web client, e.g.::
|
||||
|
||||
[http]
|
||||
static_dir = /home/alice/dev/the-client
|
||||
|
||||
If the directory includes a file named ``index.html``, it will be served on the
|
||||
root of Mopidy's web server.
|
||||
|
||||
If you're making a web based client and wants to do server side development as
|
||||
well, you are of course free to run your own web server and just use Mopidy's
|
||||
web server for the APIs. But, for clients implemented purely in JavaScript,
|
||||
letting Mopidy host the files is a simpler solution.
|
||||
|
||||
If you're looking for a web based client for Mopidy, go check out
|
||||
:ref:`http-clients`.
|
||||
77
docs/ext/index.rst
Normal file
77
docs/ext/index.rst
Normal file
@ -0,0 +1,77 @@
|
||||
.. _ext:
|
||||
|
||||
**********
|
||||
Extensions
|
||||
**********
|
||||
|
||||
Here you can find a list of 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>`.
|
||||
|
||||
|
||||
Bundled with Mopidy
|
||||
===================
|
||||
|
||||
These extensions are maintained by Mopidy's core developers. They are installed
|
||||
together with Mopidy and are enabled by default.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
**
|
||||
|
||||
|
||||
External extensions
|
||||
===================
|
||||
|
||||
These extensions are maintained outside Mopidy's core, often by other
|
||||
developers.
|
||||
|
||||
|
||||
Mopidy-Beets
|
||||
------------
|
||||
|
||||
Provides a backend for playing music from your `Beets
|
||||
<http://beets.radbox.org/>`_ music library through Beets' web extension.
|
||||
|
||||
Author:
|
||||
Janez Troha
|
||||
PyPI:
|
||||
`Mopidy-Beets <https://pypi.python.org/pypi/Mopidy-Beets>`_
|
||||
GitHub:
|
||||
`dz0ny/mopidy-beets <https://github.com/dz0ny/mopidy-beets>`_
|
||||
Issues:
|
||||
https://github.com/dz0ny/mopidy-beets/issues
|
||||
|
||||
|
||||
Mopidy-NAD
|
||||
----------
|
||||
|
||||
Extension for controlling volume using an external NAD amplifier.
|
||||
|
||||
Author:
|
||||
Stein Magnus Jodal
|
||||
PyPI:
|
||||
`Mopidy-NAD <https://pypi.python.org/pypi/Mopidy-NAD>`_
|
||||
GitHub:
|
||||
`mopidy/mopidy-nad <https://github.com/mopidy/mopidy-nad>`_
|
||||
Issues:
|
||||
https://github.com/mopidy/mopidy/issues
|
||||
|
||||
|
||||
Mopidy-SoundCloud
|
||||
-----------------
|
||||
|
||||
Provides a backend for playing music from the `SoundCloud
|
||||
<http://www.soundcloud.com/>`_ service.
|
||||
|
||||
Author:
|
||||
Janez Troha
|
||||
PyPI:
|
||||
`Mopidy-SoundCloud <https://pypi.python.org/pypi/Mopidy-SoundCloud>`_
|
||||
GitHub:
|
||||
`dz0ny/mopidy-soundcloud <https://github.com/dz0ny/mopidy-soundcloud>`_
|
||||
Issues:
|
||||
https://github.com/dz0ny/mopidy-soundcloud/issues
|
||||
86
docs/ext/local.rst
Normal file
86
docs/ext/local.rst
Normal file
@ -0,0 +1,86 @@
|
||||
.. _ext-local:
|
||||
|
||||
************
|
||||
Mopidy-Local
|
||||
************
|
||||
|
||||
Extension for playing music from a local music archive.
|
||||
|
||||
This backend handles URIs starting with ``file:``.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Local+backend
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/local/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: local/enabled
|
||||
|
||||
If the local extension should be enabled or not.
|
||||
|
||||
.. confval:: local/media_dir
|
||||
|
||||
Path to directory with local media files.
|
||||
|
||||
.. confval:: local/playlists_dir
|
||||
|
||||
Path to playlists directory with m3u files for local media.
|
||||
|
||||
.. confval:: local/tag_cache_file
|
||||
|
||||
Path to tag cache for local media.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
If you want use Mopidy to play music you have locally at your machine, you need
|
||||
to review and maybe change some of the local extension config values. See above
|
||||
for a complete list. Then you need to generate a tag cache for your local
|
||||
music...
|
||||
|
||||
|
||||
.. _generating-a-tag-cache:
|
||||
|
||||
Generating a tag cache
|
||||
----------------------
|
||||
|
||||
The program :command:`mopidy-scan` will scan the path set in the
|
||||
:confval:`local/media_dir` config value for any media files and build a MPD
|
||||
compatible ``tag_cache``.
|
||||
|
||||
To make a ``tag_cache`` of your local music available for Mopidy:
|
||||
|
||||
#. Ensure that the :confval:`local/media_dir` config value points to where your
|
||||
music is located. Check the current setting by running::
|
||||
|
||||
mopidy --show-config
|
||||
|
||||
#. Scan your media library. The command outputs the ``tag_cache`` to
|
||||
standard output, which means that you will need to redirect the output to a
|
||||
file yourself::
|
||||
|
||||
mopidy-scan > tag_cache
|
||||
|
||||
#. Move the ``tag_cache`` file to the location
|
||||
set in the :confval:`local/tag_cache_file` config value, or change the
|
||||
config value to point to where your ``tag_cache`` file is.
|
||||
|
||||
#. Start Mopidy, find the music library in a client, and play some local music!
|
||||
123
docs/ext/mpd.rst
Normal file
123
docs/ext/mpd.rst
Normal file
@ -0,0 +1,123 @@
|
||||
.. _ext-mpd:
|
||||
|
||||
**********
|
||||
Mopidy-MPD
|
||||
**********
|
||||
|
||||
This extension implements an MPD server to make Mopidy available to :ref:`MPD
|
||||
clients <mpd-clients>`.
|
||||
|
||||
MPD stands for Music Player Daemon, which is also the name of the `original MPD
|
||||
server project <http://mpd.wikia.com/>`_. Mopidy does not depend on the
|
||||
original MPD server, but implements the MPD protocol itself, and is thus
|
||||
compatible with clients for the original MPD server.
|
||||
|
||||
For more details on our MPD server implementation, see
|
||||
:mod:`mopidy.frontends.mpd`.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=MPD+frontend
|
||||
|
||||
|
||||
Limitations
|
||||
===========
|
||||
|
||||
This is a non exhaustive list of MPD features that Mopidy doesn't support.
|
||||
Items on this list will probably not be supported in the near future.
|
||||
|
||||
- Toggling of audio outputs is not supported
|
||||
- Channels for client-to-client communication are not supported
|
||||
- Stickers are not supported
|
||||
- Crossfade is not supported
|
||||
- Replay gain is not supported
|
||||
- ``count`` does not provide any statistics
|
||||
- ``stats`` does not provide any statistics
|
||||
- ``list`` does not support listing tracks by genre
|
||||
- ``decoders`` does not provide information about available decoders
|
||||
|
||||
The following items are currently not supported, but should be added in the
|
||||
near future:
|
||||
|
||||
- Modifying stored playlists is not supported
|
||||
- ``tagtypes`` is not supported
|
||||
- Browsing the file system is not supported
|
||||
- Live update of the music database is not supported
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/mpd/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: mpd/enabled
|
||||
|
||||
If the MPD extension should be enabled or not.
|
||||
|
||||
.. confval:: mpd/hostname
|
||||
|
||||
Which address the MPD server should bind to.
|
||||
|
||||
``127.0.0.1``
|
||||
Listens only on the IPv4 loopback interface
|
||||
``::1``
|
||||
Listens only on the IPv6 loopback interface
|
||||
``0.0.0.0``
|
||||
Listens on all IPv4 interfaces
|
||||
``::``
|
||||
Listens on all interfaces, both IPv4 and IPv6
|
||||
|
||||
.. confval:: mpd/port
|
||||
|
||||
Which TCP port the MPD server should listen to.
|
||||
|
||||
.. confval:: mpd/password
|
||||
|
||||
The password required for connecting to the MPD server. If blank, no
|
||||
password is required.
|
||||
|
||||
.. confval:: mpd/max_connections
|
||||
|
||||
The maximum number of concurrent connections the MPD server will accept.
|
||||
|
||||
.. confval:: mpd/connection_timeout
|
||||
|
||||
Number of seconds an MPD client can stay inactive before the connection is
|
||||
closed by the server.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default. To connect to the server, use an :ref:`MPD
|
||||
client <mpd-clients>`.
|
||||
|
||||
|
||||
.. _use-mpd-on-a-network:
|
||||
|
||||
Connecting from other machines on the network
|
||||
---------------------------------------------
|
||||
|
||||
As a secure default, Mopidy only accepts connections from ``localhost``. If you
|
||||
want to open it for connections from other machines on your network, see
|
||||
the documentation for the :confval:`mpd/hostname` config value.
|
||||
|
||||
If you open up Mopidy for your local network, you should consider turning on
|
||||
MPD password authentication by setting the :confval:`mpd/password` config value
|
||||
to the password you want to use. If the password is set, Mopidy will require
|
||||
MPD clients to provide the password before they can do anything else. Mopidy
|
||||
only supports a single password, and do not support different permission
|
||||
schemes like the original MPD server.
|
||||
105
docs/ext/mpris.rst
Normal file
105
docs/ext/mpris.rst
Normal file
@ -0,0 +1,105 @@
|
||||
.. _ext-mpris:
|
||||
|
||||
************
|
||||
Mopidy-MPRIS
|
||||
************
|
||||
|
||||
This extension lets you control Mopidy through the Media Player Remote
|
||||
Interfacing Specification (`MPRIS <http://www.mpris.org/>`_) D-Bus interface.
|
||||
|
||||
An example of an MPRIS client is the :ref:`ubuntu-sound-menu`.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- D-Bus Python bindings. The package is named ``python-dbus`` in
|
||||
Ubuntu/Debian.
|
||||
|
||||
- ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
|
||||
Ubuntu Sound Menu. The package is named ``python-indicate`` in
|
||||
Ubuntu/Debian.
|
||||
|
||||
- An ``.desktop`` file for Mopidy installed at the path set in the
|
||||
:confval:`mpris/desktop_file` config value. See usage section below for
|
||||
details.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/mpris/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: mpris/enabled
|
||||
|
||||
If the MPRIS extension should be enabled or not.
|
||||
|
||||
.. confval:: mpris/desktop_file
|
||||
|
||||
Location of the Mopidy ``.desktop`` file.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default if all dependencies are available.
|
||||
|
||||
|
||||
Controlling Mopidy through the Ubuntu Sound Menu
|
||||
------------------------------------------------
|
||||
|
||||
If you are running Ubuntu and installed Mopidy using the Debian package from
|
||||
APT you should be able to control Mopidy through the :ref:`ubuntu-sound-menu`
|
||||
without any changes.
|
||||
|
||||
If you installed Mopidy in any other way and want to control Mopidy through the
|
||||
Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be
|
||||
found in the ``data/`` dir of the Mopidy source repo into the
|
||||
``/usr/share/applications`` dir by hand::
|
||||
|
||||
cd /path/to/mopidy/source
|
||||
sudo cp data/mopidy.desktop /usr/share/applications/
|
||||
|
||||
If the correct path to the installed ``mopidy.desktop`` file on your system
|
||||
isn't ``/usr/share/applications/mopidy.conf``, you'll need to set the
|
||||
:confval:`mpris/desktop_file` config value.
|
||||
|
||||
After you have installed the file, start Mopidy in any way, and Mopidy should
|
||||
appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed
|
||||
in the Ubuntu Sound Menu, and may be restarted by selecting it there.
|
||||
|
||||
The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend. The MPRIS
|
||||
frontend supports the minimum requirements of the `MPRIS specification
|
||||
<http://www.mpris.org/>`_. The ``TrackList`` interface of the spec is not
|
||||
supported.
|
||||
|
||||
|
||||
Testing the MPRIS API directly
|
||||
------------------------------
|
||||
|
||||
To use the MPRIS API directly, start Mopidy, and then run the following in a
|
||||
Python shell::
|
||||
|
||||
import dbus
|
||||
bus = dbus.SessionBus()
|
||||
player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
|
||||
'/org/mpris/MediaPlayer2')
|
||||
|
||||
Now you can control Mopidy through the player object. Examples:
|
||||
|
||||
- To get some properties from Mopidy, run::
|
||||
|
||||
props = player.GetAll('org.mpris.MediaPlayer2',
|
||||
dbus_interface='org.freedesktop.DBus.Properties')
|
||||
|
||||
- To quit Mopidy through D-Bus, run::
|
||||
|
||||
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
|
||||
|
||||
For details on the API, please refer to the `MPRIS specification
|
||||
<http://www.mpris.org/>`_.
|
||||
53
docs/ext/scrobbler.rst
Normal file
53
docs/ext/scrobbler.rst
Normal file
@ -0,0 +1,53 @@
|
||||
****************
|
||||
Mopidy-Scrobbler
|
||||
****************
|
||||
|
||||
This extension scrobbles the music you play to your `Last.fm
|
||||
<http://www.last.fm>`_ profile.
|
||||
|
||||
.. note::
|
||||
|
||||
This extension requires a free user account at Last.fm.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
.. literalinclude:: ../../requirements/scrobbler.txt
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/scrobbler/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: scrobbler/enabled
|
||||
|
||||
If the scrobbler extension should be enabled or not.
|
||||
|
||||
.. confval:: scrobbler/username
|
||||
|
||||
Your Last.fm username.
|
||||
|
||||
.. confval:: scrobbler/password
|
||||
|
||||
Your Last.fm password.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default if all dependencies are available. You just
|
||||
need to add your Last.fm username and password to the
|
||||
``~/.config/mopidy/mopidy.conf`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[scrobbler]
|
||||
username = myusername
|
||||
password = mysecret
|
||||
83
docs/ext/spotify.rst
Normal file
83
docs/ext/spotify.rst
Normal file
@ -0,0 +1,83 @@
|
||||
.. _ext-spotify:
|
||||
|
||||
**************
|
||||
Mopidy-Spotify
|
||||
**************
|
||||
|
||||
An extension for playing music from Spotify.
|
||||
|
||||
`Spotify <http://www.spotify.com/>`_ is a music streaming service. The backend
|
||||
uses the official `libspotify
|
||||
<http://developer.spotify.com/en/libspotify/overview/>`_ library and the
|
||||
`pyspotify <http://github.com/mopidy/pyspotify/>`_ Python bindings for
|
||||
libspotify. This backend handles URIs starting with ``spotify:``.
|
||||
|
||||
.. note::
|
||||
|
||||
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
|
||||
otherwise approved in any way by Spotify. Spotify is the registered
|
||||
trade mark of the Spotify Group.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
.. literalinclude:: ../../requirements/spotify.txt
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/spotify/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: spotify/enabled
|
||||
|
||||
If the Spotify extension should be enabled or not.
|
||||
|
||||
.. confval:: spotify/username
|
||||
|
||||
Your Spotify Premium username.
|
||||
|
||||
.. confval:: spotify/password
|
||||
|
||||
Your Spotify Premium password.
|
||||
|
||||
.. confval:: spotify/bitrate
|
||||
|
||||
The preferred audio bitrate. Valid values are 96, 160, 320.
|
||||
|
||||
.. confval:: spotify/timeout
|
||||
|
||||
Max number of seconds to wait for Spotify operations to complete.
|
||||
|
||||
.. confval:: spotify/cache_dir
|
||||
|
||||
Path to the Spotify data cache. Cannot be shared with other Spotify apps.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
If you are using the Spotify backend, which is the default, enter your Spotify
|
||||
Premium account's username and password into ``~/.config/mopidy/mopidy.conf``,
|
||||
like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[spotify]
|
||||
username = myusername
|
||||
password = mysecret
|
||||
|
||||
This will only work if you have the Spotify Premium subscription. Spotify
|
||||
Unlimited will not work.
|
||||
56
docs/ext/stream.rst
Normal file
56
docs/ext/stream.rst
Normal file
@ -0,0 +1,56 @@
|
||||
.. _ext-stream:
|
||||
|
||||
*************
|
||||
Mopidy-Stream
|
||||
*************
|
||||
|
||||
Extension for playing streaming music.
|
||||
|
||||
The stream backend will handle streaming of URIs matching the
|
||||
:confval:`stream/protocols` config value, assuming the needed GStreamer plugins
|
||||
are installed.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Stream+backend
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/stream/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: stream/enabled
|
||||
|
||||
If the stream extension should be enabled or not.
|
||||
|
||||
.. confval:: stream/protocols
|
||||
|
||||
Whitelist of URI schemas to allow streaming from.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
This backend does not provide a library or similar. It simply takes any URI
|
||||
added to Mopidy's tracklist that matches any of the protocols in the
|
||||
:confval:`stream/protocols` setting and tries to play back the URI using
|
||||
GStreamer. E.g. if you're using an MPD client, you'll just have to find your
|
||||
clients "add URI" interface, and provide it with the direct URI of the stream.
|
||||
|
||||
Currently the stream backend can only work with URIs pointing direcly at
|
||||
streams, and not intermediate playlists which is often used. See :issue:`303`
|
||||
to track the development of playlist expansion support.
|
||||
@ -1,17 +1,12 @@
|
||||
.. _extensiondev:
|
||||
|
||||
*********************
|
||||
Extension development
|
||||
*********************
|
||||
|
||||
.. warning:: Draft
|
||||
|
||||
This document is a draft open for discussion. It shows how we imagine that
|
||||
development of Mopidy extensions should become in the future, not how to
|
||||
currently develop an extension for Mopidy.
|
||||
|
||||
|
||||
Mopidy started as simply an MPD server that could play music from Spotify.
|
||||
Early on Mopidy got multiple "frontends" to expose Mopidy to more than just MPD
|
||||
clients: for example the Last.fm frontend what scrobbles what you've listened
|
||||
clients: for example the scrobbler frontend what scrobbles what you've listened
|
||||
to to your Last.fm account, the MPRIS frontend that integrates Mopidy into the
|
||||
Ubuntu Sound Menu, and the HTTP server and JavaScript player API making web
|
||||
based Mopidy clients possible. In Mopidy 0.9 we added support for multiple
|
||||
@ -30,22 +25,21 @@ extension to behave.
|
||||
Anatomy of an extension
|
||||
=======================
|
||||
|
||||
Extensions are all located in a Python package called ``mopidy_something``
|
||||
where "something" is the name of the application, library or web service you
|
||||
want to integrated with Mopidy. So for example if you plan to add support for a
|
||||
service named Soundspot to Mopidy, you would name your extension's Python
|
||||
package ``mopidy_soundspot``.
|
||||
Extensions are located in a Python package called ``mopidy_something`` where
|
||||
"something" is the name of the application, library or web service you want to
|
||||
integrated with Mopidy. So for example if you plan to add support for a service
|
||||
named Soundspot to Mopidy, you would name your extension's Python package
|
||||
``mopidy_soundspot``.
|
||||
|
||||
The name of the actual extension (the human readable name) however would be
|
||||
something like "Mopidy-Soundspot". Make sure to include the name "Mopidy"
|
||||
The extension must be shipped with a ``setup.py`` file and be registered on
|
||||
`PyPI <https://pypi.python.org/>`_. The name of the distribution on PyPI would
|
||||
be something like "Mopidy-Soundspot". Make sure to include the name "Mopidy"
|
||||
somewhere in that name and that you check the capitalization. This is the name
|
||||
users will use when they install your extension from PyPI.
|
||||
|
||||
The extension must be shipped with a ``setup.py`` file and be registered on
|
||||
`PyPI <https://pypi.python.org/>`_. Also make sure the development version link
|
||||
in your package details work so that people can easily install the development
|
||||
version into their virtualenv simply by running e.g. ``pip install
|
||||
Mopidy-Soundspot==dev``.
|
||||
Also make sure the development version link in your package details work so
|
||||
that people can easily install the development version into their virtualenv
|
||||
simply by running e.g. ``pip install Mopidy-Soundspot==dev``.
|
||||
|
||||
Mopidy extensions must be licensed under an Apache 2.0 (like Mopidy itself),
|
||||
BSD, MIT or more liberal license to be able to be enlisted in the Mopidy
|
||||
@ -57,10 +51,11 @@ extension, Mopidy-Soundspot::
|
||||
|
||||
mopidy-soundspot/ # The Git repo root
|
||||
LICENSE # The license text
|
||||
MANIFEST.in # List of data files to include in PyPI package
|
||||
README.rst # Document what it is and how to use it
|
||||
mopidy_soundspot/ # Your code
|
||||
__init__.py
|
||||
config.ini # Default configuration for the extension
|
||||
ext.conf # Default config for the extension
|
||||
...
|
||||
setup.py # Installation script
|
||||
|
||||
@ -73,8 +68,8 @@ Example README.rst
|
||||
The README file should quickly tell what the extension does, how to install it,
|
||||
and how to configure it. The README should contain a development snapshot link
|
||||
to a tarball of the latest development version of the extension. It's important
|
||||
that the development snapshot link ends with ``#egg=mopidy-something-dev`` for
|
||||
installation using ``pip install mopidy-something==dev`` to work.
|
||||
that the development snapshot link ends with ``#egg=Mopidy-Something-dev`` for
|
||||
installation using ``pip install Mopidy-Something==dev`` to work.
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
@ -108,7 +103,7 @@ installation using ``pip install mopidy-something==dev`` to work.
|
||||
|
||||
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy-soundspot/issues>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/develop#egg=mopidy-soundspot-dev>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/develop#egg=Mopidy-Soundspot-dev>`_
|
||||
|
||||
|
||||
Example setup.py
|
||||
@ -120,18 +115,18 @@ register themselves as available Mopidy extensions when they are installed on
|
||||
your system.
|
||||
|
||||
The example below also includes a couple of convenient tricks for reading the
|
||||
package version from the source code so that it it's just defined in a single
|
||||
place, and to reuse the README file as the long description of the package for
|
||||
the PyPI registration.
|
||||
package version from the source code so that it is defined in a single place,
|
||||
and to reuse the README file as the long description of the package for the
|
||||
PyPI registration.
|
||||
|
||||
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy``, in
|
||||
addition to any other dependencies required by your extension. The
|
||||
``entry_points`` part must be included. The ``mopidy.extension`` part cannot be
|
||||
``entry_points`` part must be included. The ``mopidy.ext`` part cannot be
|
||||
changed, but the innermost string should be changed. It's format is
|
||||
``my_ext_name = my_py_module:MyExtClass``. ``my_ext_name`` should be a short
|
||||
``ext_name = package_name:Extension``. ``ext_name`` should be a short
|
||||
name for your extension, typically the part after "Mopidy-" in lowercase. This
|
||||
name is used e.g. to name the config section for your extension. The
|
||||
``my_py_module:MyExtClass`` part is simply the Python path to the extension
|
||||
``package_name:Extension`` part is simply the Python path to the extension
|
||||
class that will connect the rest of the dots.
|
||||
|
||||
::
|
||||
@ -158,19 +153,15 @@ class that will connect the rest of the dots.
|
||||
description='Very short description',
|
||||
long_description=open('README.rst').read(),
|
||||
packages=['mopidy_soundspot'],
|
||||
# If you ship package instead of a single module instead, use
|
||||
# 'py_modules' instead of 'packages':
|
||||
#py_modules=['mopidy_soundspot'],
|
||||
zip_safe=False,
|
||||
include_package_data=True,
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'setuptools',
|
||||
'Mopidy',
|
||||
'pysoundspot',
|
||||
],
|
||||
entry_points={
|
||||
'mopidy.extension': [
|
||||
'mopidy.ext': [
|
||||
'soundspot = mopidy_soundspot:Extension',
|
||||
],
|
||||
},
|
||||
@ -184,20 +175,49 @@ class that will connect the rest of the dots.
|
||||
],
|
||||
)
|
||||
|
||||
To make sure your README, license file and default config file is included in
|
||||
the package that is uploaded to PyPI, we'll also need to add a ``MANIFEST.in``
|
||||
file::
|
||||
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include README.rst
|
||||
include mopidy_soundspot/ext.conf
|
||||
|
||||
For details on the ``MANIFEST.in`` file format, check out the `distuitls docs
|
||||
<http://docs.python.org/2/distutils/sourcedist.html#manifest-template>`_.
|
||||
|
||||
|
||||
Example __init__.py
|
||||
===================
|
||||
|
||||
The ``__init__.py`` file should be placed inside the ``mopidy_soundspot``
|
||||
Python package. The root of your Python package should have an ``__version__``
|
||||
attribute with a :pep:`386` compliant version number, for example "0.1". Next,
|
||||
it should have a class named ``Extension`` which inherits from Mopidy's
|
||||
extension base class. This is the class referred to in the ``entry_points``
|
||||
part of ``setup.py``. Any imports of other files in your extension should be
|
||||
kept inside methods. This ensures that this file can be imported without
|
||||
raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
||||
Python package.
|
||||
|
||||
::
|
||||
The root of your Python package should have an ``__version__`` attribute with a
|
||||
:pep:`386` compliant version number, for example "0.1". Next, it should have a
|
||||
class named ``Extension`` which inherits from Mopidy's extension base class,
|
||||
:class:`mopidy.ext.Extension`. This is the class referred to in the
|
||||
``entry_points`` part of ``setup.py``. Any imports of other files in your
|
||||
extension should be kept inside methods. This ensures that this file can be
|
||||
imported without raising :exc:`ImportError` exceptions for missing
|
||||
dependencies, etc.
|
||||
|
||||
The default configuration for the extension is defined by the
|
||||
``get_default_config()`` method in the ``Extension`` class which returns a
|
||||
:mod:`ConfigParser` compatible config section. The config section's name must
|
||||
be the same as the extension's short name, as defined in the ``entry_points``
|
||||
part of ``setup.py``, for example ``soundspot``. All extensions must include
|
||||
an ``enabled`` config which normally should default to ``true``. Provide good
|
||||
defaults for all config values so that as few users as possible will need to
|
||||
change them. The exception is if the config value has security implications; in
|
||||
that case you should default to the most secure configuration. Leave any
|
||||
configurations that doesn't have meaningful defaults blank, like ``username``
|
||||
and ``password``. In the example below, we've chosen to maintain the default
|
||||
config as a separate file named ``ext.conf``. This makes it easy to e.g.
|
||||
include the default config in documentation without duplicating it.
|
||||
|
||||
This is ``mopidy_soundspot/__init__.py``::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
@ -208,8 +228,7 @@ raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
from mopidy.exceptions import ExtensionError
|
||||
from mopidy.utils import ext
|
||||
from mopidy import config, exceptions, ext
|
||||
|
||||
|
||||
__version__ = '0.1'
|
||||
@ -217,73 +236,44 @@ raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
name = 'Mopidy-Soundspot'
|
||||
dist_name = 'Mopidy-Soundspot'
|
||||
ext_name = 'soundspot'
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
config_file = os.path.join(
|
||||
os.path.dirname(__file__), 'config.ini')
|
||||
return open(config_file).read()
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__, 'ext.conf'))
|
||||
return config.read(conf_file)
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls, config):
|
||||
# ``config`` is the complete config document for the Mopidy
|
||||
# instance. The extension is free to check any config value it is
|
||||
# interested in, not just its own config values.
|
||||
|
||||
if not config.getboolean('soundspot', 'enabled'):
|
||||
return
|
||||
if not config.get('soundspot', 'username'):
|
||||
raise ExtensionError('Config soundspot.username not set')
|
||||
if not config.get('soundspot', 'password'):
|
||||
raise ExtensionError('Config soundspot.password not set')
|
||||
|
||||
@classmethod
|
||||
def validate_environment(cls):
|
||||
# This method can validate anything it wants about the environment
|
||||
# the extension is running in. Examples include checking if all
|
||||
# dependencies are installed.
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['username'] = config.String()
|
||||
schema['password'] = config.Secret()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
try:
|
||||
import pysoundspot
|
||||
except ImportError as e:
|
||||
raise ExtensionError('pysoundspot library not found', e)
|
||||
raise exceptions.ExtensionError('pysoundspot library not found', e)
|
||||
|
||||
# You will typically only implement one of the next three methods
|
||||
# in a single extension.
|
||||
|
||||
@classmethod
|
||||
def get_frontend_class(cls):
|
||||
def get_frontend_classes(self):
|
||||
from .frontend import SoundspotFrontend
|
||||
return SoundspotFrontend
|
||||
return [SoundspotFrontend]
|
||||
|
||||
@classmethod
|
||||
def get_backend_class(cls):
|
||||
def get_backend_classes(self):
|
||||
from .backend import SoundspotBackend
|
||||
return SoundspotBackend
|
||||
return [SoundspotBackend]
|
||||
|
||||
@classmethod
|
||||
def register_gstreamer_elements(cls):
|
||||
def register_gstreamer_elements(self):
|
||||
from .mixer import SoundspotMixer
|
||||
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||
|
||||
|
||||
|
||||
Example config.ini
|
||||
==================
|
||||
|
||||
The default configuration for the extension is located in a ``config.ini`` file
|
||||
inside the Python package. It contains a single config section, with a name
|
||||
matching the short name used for the extension in the ``entry_points`` part of
|
||||
``setup.py``.
|
||||
|
||||
All extensions should include an ``enabled`` config which should default to
|
||||
``true``. Leave any configurations that doesn't have meaningful defaults blank,
|
||||
like ``username`` and ``password``.
|
||||
And this is ``mopidy_soundspot/ext.conf``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@ -292,6 +282,8 @@ like ``username`` and ``password``.
|
||||
username =
|
||||
password =
|
||||
|
||||
For more detailed documentation on the extension class, see the :ref:`ext-api`.
|
||||
|
||||
|
||||
Example frontend
|
||||
================
|
||||
@ -350,61 +342,57 @@ If you want to extend Mopidy's GStreamer pipeline with new custom GStreamer
|
||||
elements, you'll need to register them in GStreamer before they can be used.
|
||||
|
||||
Basically, you just implement your GStreamer element in Python and then make
|
||||
your :meth:`Extension.register_gstreamer_elements` method register all your
|
||||
custom GStreamer elements.
|
||||
your :meth:`~mopidy.ext.Extension.register_gstreamer_elements` method register
|
||||
all your custom GStreamer elements.
|
||||
|
||||
For examples of custom GStreamer elements implemented in Python, see
|
||||
:mod:`mopidy.audio.mixers`.
|
||||
|
||||
|
||||
Implementation steps
|
||||
====================
|
||||
Python conventions
|
||||
==================
|
||||
|
||||
A rough plan of how to make the above document the reality of how Mopidy
|
||||
extensions work.
|
||||
In general, it would be nice if Mopidy extensions followed the same
|
||||
:ref:`codestyle` as Mopidy itself, as they're part of the same ecosystem. Among
|
||||
other things, the code style guide explains why all the above examples start
|
||||
with ``from __future__ import unicode_literals``.
|
||||
|
||||
1. Implement :class:`mopidy.utils.ext.Extension` base class and the
|
||||
:exc:`mopidy.exceptions.ExtensionError` exception class.
|
||||
|
||||
2. Switch from using distutils to setuptools to package and install Mopidy so
|
||||
that we can register entry points for the bundled extensions and get
|
||||
information about all extensions available on the system from
|
||||
:mod:`pkg_resources`.
|
||||
Use of Mopidy APIs
|
||||
==================
|
||||
|
||||
3. Add :class:`Extension` classes for all existing frontends and backends. Make
|
||||
sure to add default config files and config validation, even though this
|
||||
will not be used at this implementation stage.
|
||||
When writing an extension, you should only use APIs documented at
|
||||
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.utils`, may change at
|
||||
any time, and is not something extensions should rely on being stable.
|
||||
|
||||
4. Add entry points for the existing extensions in the ``setup.py`` file.
|
||||
|
||||
5. Rewrite the startup procedure to find extensions and thus frontends and
|
||||
backends via :mod:`pkg_resouces` instead of the ``FRONTENDS`` and
|
||||
``BACKENDS`` settings.
|
||||
Logging in extensions
|
||||
=====================
|
||||
|
||||
6. Remove the ``FRONTENDS`` and ``BACKENDS`` settings.
|
||||
When making servers like Mopidy, logging is essential for understanding what's
|
||||
going on. We use the :mod:`logging` module from Python's standard library. When
|
||||
creating a logger, always namespace the logger using your Python package name
|
||||
as this will be visible in Mopidy's debug log::
|
||||
|
||||
7. Switch to ini file based configuration, using :mod:`ConfigParser`. The
|
||||
default config is the combination of a core config file plus the config from
|
||||
each installed extension. To find the effective config for the system, the
|
||||
following config sources are added together, with the later ones overriding
|
||||
the earlier ones:
|
||||
import logging
|
||||
|
||||
- the default config built from Mopidy core and all installed extensions,
|
||||
logger = logging.getLogger('mopidy_soundspot')
|
||||
|
||||
- ``/etc/mopidy.conf``,
|
||||
When logging at logging level ``info`` or higher (i.e. ``warning``, ``error``,
|
||||
and ``critical``, but not ``debug``) the log message will be displayed to all
|
||||
Mopidy users. Thus, the log messages at those levels should be well written and
|
||||
easy to understand.
|
||||
|
||||
- ``~/.config/mopidy.conf``,
|
||||
As the logger name is not included in Mopidy's default logging format, you
|
||||
should make it obvious from the log message who is the source of the log
|
||||
message. For example::
|
||||
|
||||
- any config file provided via command line arguments, and
|
||||
Loaded 17 Soundspot playlists
|
||||
|
||||
- any config values provided via command line arguments.
|
||||
Is much better than::
|
||||
|
||||
8. Add command line options for:
|
||||
Loaded 17 playlists
|
||||
|
||||
- loading an additional config file for this execution of Mopidy,
|
||||
|
||||
- setting a config value for this execution of Mopidy,
|
||||
|
||||
- printing the effective config and exit, and
|
||||
|
||||
- write a config value permanently to ``~/.config/mopidy.conf`` and exit.
|
||||
If you want to turn on debug logging for your own extension, but not for
|
||||
everything else due to the amount of noise, see the docs for the
|
||||
:confval:`loglevels/*` config section.
|
||||
|
||||
@ -2,56 +2,71 @@
|
||||
Mopidy
|
||||
******
|
||||
|
||||
Mopidy is a music server which can play music both from your :ref:`local hard
|
||||
drive <local-backend>` and from :ref:`Spotify <spotify-backend>`. Searches
|
||||
returns results from both your local hard drive and from Spotify, and you can
|
||||
mix tracks from both sources in your play queue. Your Spotify playlists are
|
||||
also available for use, though we don't support modifying them yet.
|
||||
Mopidy is a music server which can play music both from multiple sources, like
|
||||
your :ref:`local hard drive <ext-local>`, :ref:`radio streams <ext-stream>`,
|
||||
and from :ref:`Spotify <ext-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.
|
||||
|
||||
To control your music server, you can use the :ref:`Ubuntu Sound Menu
|
||||
<ubuntu-sound-menu>` on the machine running Mopidy, any device on the same
|
||||
network which can control UPnP MediaRenderers (see :ref:`upnp-clients`), or any
|
||||
:ref:`MPD client <mpd-clients>`. MPD clients are available for most platforms,
|
||||
including Windows, Mac OS X, Linux, Android, and iOS.
|
||||
To control your Mopidy music server, you can use one of Mopidy's :ref:`web
|
||||
clients <http-clients>`, the :ref:`Ubuntu Sound Menu <ubuntu-sound-menu>`, any
|
||||
device on the same network which can control :ref:`UPnP MediaRenderers
|
||||
<upnp-clients>`, or any :ref:`MPD client <mpd-clients>`. MPD clients are
|
||||
available for many platforms, including Windows, OS X, Linux, Android and iOS.
|
||||
|
||||
To install Mopidy, start by reading :ref:`installation`.
|
||||
To get started with Mopidy, start by reading :ref:`installation`.
|
||||
|
||||
If you get stuck, we usually hang around at ``#mopidy`` at `irc.freenode.net
|
||||
<http://freenode.net/>`_ and also got a `mailing list at Google Groups
|
||||
<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 the `issue
|
||||
tracker <https://github.com/mopidy/mopidy/issues>`_.
|
||||
tracker <https://github.com/mopidy/mopidy/issues>`_. The `source code
|
||||
<https://github.com/mopidy/mopidy>`_ may also be of help. If you want to stay
|
||||
up to date on Mopidy developments, you can follow `@mopidy
|
||||
<https://twitter.com/mopidy/>`_ on Twitter.
|
||||
|
||||
|
||||
Project resources
|
||||
=================
|
||||
|
||||
- `Documentation <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>`_
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
|
||||
|
||||
User documentation
|
||||
==================
|
||||
Usage
|
||||
=====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 3
|
||||
|
||||
installation/index
|
||||
installation/raspberrypi
|
||||
settings
|
||||
config
|
||||
ext/index
|
||||
running
|
||||
clients/index
|
||||
troubleshooting
|
||||
|
||||
|
||||
About
|
||||
=====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
authors
|
||||
licenses
|
||||
changes
|
||||
changelog
|
||||
versioning
|
||||
|
||||
|
||||
Reference documentation
|
||||
=======================
|
||||
Development
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
contributing
|
||||
devtools
|
||||
codestyle
|
||||
extensiondev
|
||||
|
||||
|
||||
Reference
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
@ -60,16 +75,6 @@ Reference documentation
|
||||
modules/index
|
||||
|
||||
|
||||
Development documentation
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
development
|
||||
extensiondev
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ in the same way as you get updates to the rest of your distribution.
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
#. 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
|
||||
@ -71,9 +71,10 @@ it out.
|
||||
Arch Linux: Install from AUR
|
||||
============================
|
||||
|
||||
If you are running Arch Linux, you can install a development snapshot of Mopidy
|
||||
If you are running Arch Linux, you can install the latest release of Mopidy
|
||||
using the `mopidy-git <https://aur.archlinux.org/packages/mopidy-git/>`_
|
||||
package found in AUR.
|
||||
package found in AUR. The package installs from the ``master`` branch of the
|
||||
Mopidy Git repo, which always corresponds to the latest release.
|
||||
|
||||
#. To install Mopidy with GStreamer, libspotify and pyspotify, you can use
|
||||
``packer``, ``yaourt``, or do it by hand like this::
|
||||
@ -89,8 +90,8 @@ package found in AUR.
|
||||
install `python2-pylast
|
||||
<https://aur.archlinux.org/packages/python2-pylast/>`_ from AUR.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. 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
|
||||
@ -107,13 +108,19 @@ Pip.
|
||||
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-python gst-plugins-good gst-plugins-ugly libspotify
|
||||
brew install gst-python010 gst-plugins-good010 gst-plugins-ugly010 libspotify
|
||||
|
||||
#. 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 crash.
|
||||
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``::
|
||||
@ -135,13 +142,13 @@ Pip.
|
||||
|
||||
sudo easy_install pip
|
||||
|
||||
#. Then get, build, and install the latest releast of pyspotify, pylast, pykka,
|
||||
#. Then get, build, and install the latest release of pyspotify, pylast,
|
||||
and Mopidy using Pip::
|
||||
|
||||
sudo pip install -U pyspotify pylast pykka mopidy
|
||||
sudo pip install -U pyspotify pylast cherrypy ws4py mopidy
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. 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
|
||||
@ -171,15 +178,7 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo yum install -y gcc python-devel python-pip
|
||||
|
||||
#. Then you'll need to install all of Mopidy's hard dependencies:
|
||||
|
||||
- Pykka >= 1.0::
|
||||
|
||||
sudo pip install -U pykka
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U pykka
|
||||
#. Then you'll need to install all of Mopidy's hard non-Python dependencies:
|
||||
|
||||
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
|
||||
popular Linux distributions. Search for GStreamer in your package manager,
|
||||
@ -235,7 +234,8 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo pip install -U pyspotify
|
||||
|
||||
# Fedora:
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U pyspotify
|
||||
|
||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
||||
@ -243,9 +243,19 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo pip install -U pylast
|
||||
|
||||
# Fedora:
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U pylast
|
||||
|
||||
#. Optional: If you want to use the HTTP frontend and web clients, you need
|
||||
cherrypy and ws4py::
|
||||
|
||||
sudo pip install -U cherrypy ws4py
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U cherrypy ws4py
|
||||
|
||||
#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
|
||||
Menu or from an UPnP client via Rygel, you need some additional
|
||||
dependencies: the Python bindings for libindicate, and the Python bindings
|
||||
@ -259,7 +269,8 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo pip install -U mopidy
|
||||
|
||||
# Fedora:
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U mopidy
|
||||
|
||||
To upgrade Mopidy to future releases, just rerun this command.
|
||||
@ -269,5 +280,5 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo pip install mopidy==dev
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
@ -4,13 +4,8 @@
|
||||
Installation on Raspberry Pi
|
||||
****************************
|
||||
|
||||
As of early August, 2012, running Mopidy on a `Raspberry Pi
|
||||
<http://www.raspberrypi.org/>`_ is possible, although there are a few
|
||||
significant drawbacks to doing so. This document is intended to help you get
|
||||
Mopidy running on your Raspberry Pi and to document the progress made and
|
||||
issues surrounding running Mopidy on the Raspberry Pi.
|
||||
|
||||
As of January 2013, Mopidy will run with Spotify support on both the armel
|
||||
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
|
||||
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
|
||||
distribution.
|
||||
|
||||
@ -19,167 +14,28 @@ distribution.
|
||||
:height: 427
|
||||
|
||||
|
||||
.. _raspi-squeeze:
|
||||
|
||||
How to for Debian 6 (Squeeze)
|
||||
=============================
|
||||
|
||||
The following guide illustrates how to get Mopidy running on a minimal Debian
|
||||
squeeze distribution.
|
||||
|
||||
1. The image used can be downloaded at
|
||||
http://www.linuxsystems.it/2012/06/debian-wheezy-raspberry-pi-minimal-image/.
|
||||
This image is a very minimal distribution and does not include many common
|
||||
packages you might be used to having access to. If you find yourself trying
|
||||
to complete instructions here and getting ``command not found``, try using
|
||||
``apt-get`` to install the relevant packages!
|
||||
|
||||
2. Flash the OS image to your SD card. See
|
||||
http://elinux.org/RPi_Easy_SD_Card_Setup for help.
|
||||
|
||||
3. If you have an SD card that's >2 GB, resize the disk image to use some more
|
||||
space (we'll need a bit more to install some packages and stuff). See
|
||||
http://elinux.org/RPi_Resize_Flash_Partitions#Manually_resizing_the_SD_card_on_Raspberry_Pi
|
||||
for help.
|
||||
|
||||
4. To even get to the point where we can start installing software let's create
|
||||
a new user and give it sudo access.
|
||||
|
||||
- Install ``sudo``::
|
||||
|
||||
apt-get install sudo
|
||||
|
||||
- Create a user account::
|
||||
|
||||
adduser <username>
|
||||
|
||||
- Give the user sudo access by adding it to the ``sudo`` group so we don't
|
||||
have to do everything on the ``root`` account::
|
||||
|
||||
adduser <username> sudo
|
||||
|
||||
- While we're at it, give your user access to the sound card by adding it to
|
||||
the audio group::
|
||||
|
||||
adduser <username> audio
|
||||
|
||||
- Log in to your Raspberry Pi again with your new user account instead of
|
||||
the ``root`` account.
|
||||
|
||||
5. Enable the Raspberry Pi's sound drivers:
|
||||
|
||||
- To enable the Raspberry Pi's sound driver::
|
||||
|
||||
sudo modprobe snd_bcm2835
|
||||
|
||||
- To load the sound driver at boot time::
|
||||
|
||||
echo "snd_bcm2835" | sudo tee /etc/modules
|
||||
|
||||
6. Let's get the Raspberry Pi up-to-date:
|
||||
|
||||
- Get some tools that we need to download and run the ``rpi-update``
|
||||
script::
|
||||
|
||||
sudo apt-get install ca-certificates git-core binutils
|
||||
|
||||
- Download ``rpi-update`` from Github::
|
||||
|
||||
sudo wget https://raw.github.com/Hexxeh/rpi-update/master/rpi-update
|
||||
|
||||
- Move ``rpi-update`` to an appropriate location::
|
||||
|
||||
sudo mv rpi-update /usr/local/bin/rpi-update
|
||||
|
||||
- Make ``rpi-update`` executable::
|
||||
|
||||
sudo chmod +x /usr/local/bin/rpi-update
|
||||
|
||||
- Finally! Update your firmware::
|
||||
|
||||
sudo rpi-update
|
||||
|
||||
- After firmware updating finishes, reboot your Raspberry Pi::
|
||||
|
||||
sudo reboot
|
||||
|
||||
7. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
|
||||
|
||||
- Load the IPv6 kernel module now::
|
||||
|
||||
sudo modprobe ipv6
|
||||
|
||||
- Add ``ipv6`` to ``/etc/modules`` to ensure the IPv6 kernel module is
|
||||
loaded on boot::
|
||||
|
||||
echo ipv6 | sudo tee -a /etc/modules
|
||||
|
||||
8. Installing Mopidy and its dependencies from `apt.mopidy.com
|
||||
<http://apt.mopidy.com/>`_, as described in :ref:`installation`. In short::
|
||||
|
||||
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
9. jackd2, which should be installed at this point, seems to cause some
|
||||
problems. Let's install jackd1, as it seems to work a little bit better::
|
||||
|
||||
sudo apt-get install jackd1
|
||||
|
||||
You may encounter some issues with your audio configuration where sound does
|
||||
not play. If that happens, edit your ``/etc/asound.conf`` to read something
|
||||
like::
|
||||
|
||||
pcm.mmap0 {
|
||||
type mmap_emul;
|
||||
slave {
|
||||
pcm "hw:0,0";
|
||||
}
|
||||
}
|
||||
|
||||
pcm.!default {
|
||||
type plug;
|
||||
slave {
|
||||
pcm mmap0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. _raspi-wheezy:
|
||||
|
||||
How to for Debian 7 (Wheezy)
|
||||
============================
|
||||
|
||||
This is a very similar system to Debian 6.0 above, but with a bit newer
|
||||
software packages, as Wheezy is going to be the next release of Debian.
|
||||
|
||||
1. Download the latest wheezy disk image from
|
||||
#. Download the latest wheezy disk image from
|
||||
http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
|
||||
2012-08-08.
|
||||
|
||||
2. Flash the OS image to your SD card. See
|
||||
#. Flash the OS image to your SD card. See
|
||||
http://elinux.org/RPi_Easy_SD_Card_Setup for help.
|
||||
|
||||
3. If you have an SD card that's >2 GB, you don't have to resize the file
|
||||
#. If you have an SD card that's >2 GB, you don't have to resize the file
|
||||
systems on another computer. Just boot up your Raspberry Pi with the
|
||||
unaltered partions, and it will boot right into the ``raspi-config`` tool,
|
||||
which will let you grow the root file system to fill the SD card. This tool
|
||||
will also allow you do other useful stuff, like turning on the SSH server.
|
||||
|
||||
4. As opposed to on Squeeze, ``sudo`` comes preinstalled. You can login to the
|
||||
default user using username ``pi`` and password ``raspberry``. To become
|
||||
root, just enter ``sudo -i``.
|
||||
#. You can login to the default user using username ``pi`` and password
|
||||
``raspberry``. To become root, just enter ``sudo -i``.
|
||||
|
||||
Opposed to on Squeeze, there is no need to add your user to the ``audio``
|
||||
group, as the ``pi`` user already is a member of that group.
|
||||
|
||||
5. As opposed to on Squeeze, the correct sound driver comes preinstalled.
|
||||
|
||||
6. As opposed to on Squeeze, your kernel and GPU firmware is rather up to date
|
||||
when running Wheezy.
|
||||
|
||||
7. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
|
||||
#. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
|
||||
|
||||
- Load the IPv6 kernel module now::
|
||||
|
||||
@ -190,7 +46,7 @@ software packages, as Wheezy is going to be the next release of Debian.
|
||||
|
||||
echo ipv6 | sudo tee -a /etc/modules
|
||||
|
||||
8. Installing Mopidy and its dependencies from `apt.mopidy.com
|
||||
#. Installing Mopidy and its dependencies from `apt.mopidy.com
|
||||
<http://apt.mopidy.com/>`_, as described in :ref:`installation`. In short::
|
||||
|
||||
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||||
@ -198,7 +54,7 @@ software packages, as Wheezy is going to be the next release of Debian.
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
9. Since I have a HDMI cable connected, but want the sound on the analog sound
|
||||
#. Since I have a HDMI cable connected, but want the sound on the analog sound
|
||||
connector, I have to run::
|
||||
|
||||
amixer cset numid=3 1
|
||||
@ -209,34 +65,16 @@ software packages, as Wheezy is going to be the next release of Debian.
|
||||
|
||||
aplay /usr/share/sounds/alsa/Front_Center.wav
|
||||
|
||||
If you hear a voice saying "Front Center", then your sound is working. Don't
|
||||
be concerned if this test sound includes static. Test your sound with
|
||||
GStreamer to determine the sound quality of Mopidy.
|
||||
If you hear a voice saying "Front Center", then your sound is working.
|
||||
|
||||
To make the change to analog output stick, you can add the ``amixer``
|
||||
command to e.g. ``/etc/rc.local``, which will be executed when the system is
|
||||
booting.
|
||||
|
||||
|
||||
Audio quality issues
|
||||
====================
|
||||
|
||||
The Raspberry Pi's audio quality can be sub-par through the analog output. This
|
||||
is known and unlikely to be fixed as including any higher-quality hardware
|
||||
would increase the cost of the board. If you experience crackling/hissing or
|
||||
skipping audio, you may want to try a USB sound card. Additionally, you could
|
||||
lower your default ALSA sampling rate to 22KHz, though this will lead to a
|
||||
substantial decrease in sound quality.
|
||||
Fixing audio quality issues
|
||||
===========================
|
||||
|
||||
As of January 2013, some reports also indicate that pushing the audio through
|
||||
PulseAudio may help. We hope to, in the future, provide a complete set of
|
||||
instructions here leading to acceptable analog audio quality.
|
||||
|
||||
|
||||
Support
|
||||
=======
|
||||
|
||||
If you had trouble with the above or got Mopidy working a different way on
|
||||
Raspberry Pi, please send us a pull request to update this page with your new
|
||||
information. As usual, the folks at ``#mopidy`` on ``irc.freenode.net`` may be
|
||||
able to help with any problems encountered.
|
||||
|
||||
@ -8,7 +8,7 @@ contributed what, please refer to our git repository.
|
||||
Source code license
|
||||
===================
|
||||
|
||||
Copyright 2009-2012 Stein Magnus Jodal and contributors
|
||||
Copyright 2009-2013 Stein Magnus Jodal and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,7 +26,7 @@ limitations under the License.
|
||||
Documentation license
|
||||
=====================
|
||||
|
||||
Copyright 2010-2012 Stein Magnus Jodal and contributors
|
||||
Copyright 2010-2013 Stein Magnus Jodal and contributors
|
||||
|
||||
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||
Unported License. To view a copy of this license, visit
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
*********************************************
|
||||
:mod:`mopidy.audio.mixers.nad` -- NAD mixer
|
||||
*********************************************
|
||||
|
||||
.. automodule:: mopidy.audio.mixers.nad
|
||||
:synopsis: Mixer element for controlling volume on NAD amplifiers
|
||||
@ -1,7 +0,0 @@
|
||||
*********************************************************
|
||||
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
|
||||
*********************************************************
|
||||
|
||||
.. automodule:: mopidy.backends.dummy
|
||||
:synopsis: Dummy backend used for testing
|
||||
:members:
|
||||
@ -1,8 +0,0 @@
|
||||
.. _local-backend:
|
||||
|
||||
*********************************************
|
||||
:mod:`mopidy.backends.local` -- Local backend
|
||||
*********************************************
|
||||
|
||||
.. automodule:: mopidy.backends.local
|
||||
:synopsis: Backend for playing music files on local storage
|
||||
@ -1,8 +0,0 @@
|
||||
.. _spotify-backend:
|
||||
|
||||
*************************************************
|
||||
:mod:`mopidy.backends.spotify` -- Spotify backend
|
||||
*************************************************
|
||||
|
||||
.. automodule:: mopidy.backends.spotify
|
||||
:synopsis: Backend for the Spotify music streaming service
|
||||
@ -1,7 +0,0 @@
|
||||
***********************************************
|
||||
:mod:`mopidy.backends.stream` -- Stream backend
|
||||
***********************************************
|
||||
|
||||
.. automodule:: mopidy.backends.stream
|
||||
:synopsis: Backend for playing audio streams
|
||||
:members:
|
||||
@ -1,8 +0,0 @@
|
||||
.. _http-frontend:
|
||||
|
||||
*********************************************
|
||||
:mod:`mopidy.frontends.http` -- HTTP frontend
|
||||
*********************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.http
|
||||
:synopsis: HTTP and WebSockets frontend
|
||||
@ -1,6 +0,0 @@
|
||||
***************************************************
|
||||
:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
|
||||
***************************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.lastfm
|
||||
:synopsis: Last.fm scrobbler frontend
|
||||
@ -2,6 +2,8 @@
|
||||
:mod:`mopidy.frontends.mpd` -- MPD server
|
||||
*****************************************
|
||||
|
||||
For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd
|
||||
:synopsis: MPD server frontend
|
||||
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
.. _mpris-frontend:
|
||||
|
||||
***********************************************
|
||||
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
|
||||
***********************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.mpris
|
||||
:synopsis: MPRIS frontend
|
||||
121
docs/running.rst
121
docs/running.rst
@ -18,3 +18,124 @@ using ``kill``::
|
||||
kill `ps ax | grep mopidy | grep -v grep | cut -d' ' -f1`
|
||||
|
||||
This can be useful e.g. if you create init script for managing Mopidy.
|
||||
|
||||
|
||||
mopidy command
|
||||
==============
|
||||
|
||||
.. program:: mopidy
|
||||
|
||||
.. cmdoption:: --version
|
||||
|
||||
Show Mopidy's version number and exit.
|
||||
|
||||
.. cmdoption:: -h, --help
|
||||
|
||||
Show help message and exit.
|
||||
|
||||
.. cmdoption:: -q, --quiet
|
||||
|
||||
Show less output: warning level and higher.
|
||||
|
||||
.. cmdoption:: -v, --verbose
|
||||
|
||||
Show more output: debug level and higher.
|
||||
|
||||
.. cmdoption:: --save-debug-log
|
||||
|
||||
Save debug log to the file specified in the :confval:`logging/debug_file`
|
||||
config value, typically ``./mopidy.conf``.
|
||||
|
||||
.. cmdoption:: --show-config
|
||||
|
||||
Show the current effective config. All configuration sources are merged
|
||||
together to show the effective document. Secret values like passwords are
|
||||
masked out. Config for disabled extensions are not included.
|
||||
|
||||
.. cmdoption:: --show-deps
|
||||
|
||||
Show dependencies, their versions and installation location.
|
||||
|
||||
.. cmdoption:: --config <file>
|
||||
|
||||
Specify config file to use. To use multiple config files, separate them
|
||||
with colon. The later files override the earlier ones if there's a
|
||||
conflict.
|
||||
|
||||
.. cmdoption:: -o <option>, --option <option>
|
||||
|
||||
Specify additional config values in the ``section/key=value`` format. Can
|
||||
be provided multiple times.
|
||||
|
||||
|
||||
|
||||
mopidy-scan command
|
||||
===================
|
||||
|
||||
.. program:: mopidy-scan
|
||||
|
||||
.. cmdoption:: --version
|
||||
|
||||
Show Mopidy's version number and exit.
|
||||
|
||||
.. cmdoption:: -h, --help
|
||||
|
||||
Show help message and exit.
|
||||
|
||||
.. cmdoption:: -q, --quiet
|
||||
|
||||
Show less output: warning level and higher.
|
||||
|
||||
.. cmdoption:: -v, --verbose
|
||||
|
||||
Show more output: debug level and higher.
|
||||
|
||||
|
||||
.. _mopidy-convert-config:
|
||||
|
||||
mopidy-convert-config command
|
||||
=============================
|
||||
|
||||
.. program:: mopidy-convert-config
|
||||
|
||||
This program does not take any options. It looks for the pre-0.14 settings file
|
||||
at ``$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 ``$XDG_CONFIG_DIR/mopidy/mopidy.conf``, you're asked if
|
||||
you want to save the converted config to that file.
|
||||
|
||||
Example usage::
|
||||
|
||||
$ cat ~/.config/mopidy/settings.py
|
||||
LOCAL_MUSIC_PATH = u'~/music'
|
||||
MPD_SERVER_HOSTNAME = u'::'
|
||||
SPOTIFY_PASSWORD = u'secret'
|
||||
SPOTIFY_USERNAME = u'alice'
|
||||
|
||||
$ 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.
|
||||
|
||||
$ cat ~/.config/mopidy/mopidy.conf
|
||||
[spotify]
|
||||
username = alice
|
||||
password = secret
|
||||
|
||||
[mpd]
|
||||
hostname = ::
|
||||
|
||||
[local]
|
||||
media_dir = ~/music
|
||||
|
||||
@ -1,223 +0,0 @@
|
||||
********
|
||||
Settings
|
||||
********
|
||||
|
||||
Mopidy has lots of settings. Luckily, you only need to change a few, and stay
|
||||
ignorant of the rest. Below you can find guides for typical configuration
|
||||
changes you may want to do, and a complete listing of available settings.
|
||||
|
||||
|
||||
Changing settings
|
||||
=================
|
||||
|
||||
Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where
|
||||
``~`` means your *home directory*. If your username is ``alice`` and you are
|
||||
running Linux, the settings file should probably be at
|
||||
``/home/alice/.config/mopidy/settings.py``.
|
||||
|
||||
You can either create the settings file yourself, or run the ``mopidy``
|
||||
command, and it will create an empty settings file for you.
|
||||
|
||||
When you have created the settings file, open it in a text editor, and add
|
||||
settings you want to change. If you want to keep the default value for a
|
||||
setting, you should *not* redefine it in your own settings file.
|
||||
|
||||
A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
|
||||
|
||||
MPD_SERVER_HOSTNAME = u'::'
|
||||
SPOTIFY_USERNAME = u'alice'
|
||||
SPOTIFY_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
.. _music-from-spotify:
|
||||
|
||||
Music from Spotify
|
||||
==================
|
||||
|
||||
If you are using the Spotify backend, which is the default, enter your Spotify
|
||||
Premium account's username and password into the file, like this::
|
||||
|
||||
SPOTIFY_USERNAME = u'myusername'
|
||||
SPOTIFY_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
.. _music-from-local-storage:
|
||||
|
||||
Music from local storage
|
||||
========================
|
||||
|
||||
If you want use Mopidy to play music you have locally at your machine instead
|
||||
of or in addition to using Spotify, you need to review and maybe change some of
|
||||
the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of
|
||||
available settings. Then you need to generate a tag cache for your local
|
||||
music...
|
||||
|
||||
|
||||
.. _generating-a-tag-cache:
|
||||
|
||||
Generating a tag cache
|
||||
----------------------
|
||||
|
||||
Before Mopidy 0.3 the local storage backend relied purely on ``tag_cache``
|
||||
files generated by the original MPD server. To remedy this the command
|
||||
:command:`mopidy-scan` was created. The program will scan your current
|
||||
:attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible
|
||||
``tag_cache``.
|
||||
|
||||
To make a ``tag_cache`` of your local music available for Mopidy:
|
||||
|
||||
#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_PATH` points to where your
|
||||
music is located. Check the current setting by running::
|
||||
|
||||
mopidy --list-settings
|
||||
|
||||
#. Scan your music library. The command outputs the ``tag_cache`` to
|
||||
``stdout``, which means that you will need to redirect the output to a file
|
||||
yourself::
|
||||
|
||||
mopidy-scan > tag_cache
|
||||
|
||||
#. Move the ``tag_cache`` file to the location
|
||||
:attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the
|
||||
setting to point to where your ``tag_cache`` file is.
|
||||
|
||||
#. Start Mopidy, find the music library in a client, and play some local music!
|
||||
|
||||
|
||||
.. _use-mpd-on-a-network:
|
||||
|
||||
Connecting from other machines on the network
|
||||
=============================================
|
||||
|
||||
As a secure default, Mopidy only accepts connections from ``localhost``. If you
|
||||
want to open it for connections from other machines on your network, see
|
||||
the documentation for :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`.
|
||||
|
||||
If you open up Mopidy for your local network, you should consider turning on
|
||||
MPD password authentication by setting
|
||||
:attr:`mopidy.settings.MPD_SERVER_PASSWORD` to the password you want to use.
|
||||
If the password is set, Mopidy will require MPD clients to provide the password
|
||||
before they can do anything else. Mopidy only supports a single password, and
|
||||
do not support different permission schemes like the original MPD server.
|
||||
|
||||
|
||||
Scrobbling tracks to Last.fm
|
||||
============================
|
||||
|
||||
If you want to submit the tracks you are playing to your `Last.fm
|
||||
<http://www.last.fm/>`_ profile, make sure you've installed the dependencies
|
||||
found at :mod:`mopidy.frontends.lastfm` and add the following to your settings
|
||||
file::
|
||||
|
||||
LASTFM_USERNAME = u'myusername'
|
||||
LASTFM_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
.. _install-desktop-file:
|
||||
|
||||
Controlling Mopidy through the Ubuntu Sound Menu
|
||||
================================================
|
||||
|
||||
If you are running Ubuntu and installed Mopidy using the Debian package from
|
||||
APT you should be able to control Mopidy through the `Ubuntu Sound Menu
|
||||
<https://wiki.ubuntu.com/SoundMenu>`_ without any changes.
|
||||
|
||||
If you installed Mopidy in any other way and want to control Mopidy through the
|
||||
Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be
|
||||
found in the ``data/`` dir of the Mopidy source into the
|
||||
``/usr/share/applications`` dir by hand::
|
||||
|
||||
cd /path/to/mopidy/source
|
||||
sudo cp data/mopidy.desktop /usr/share/applications/
|
||||
|
||||
After you have installed the file, start Mopidy in any way, and Mopidy should
|
||||
appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed
|
||||
in the Ubuntu Sound Menu, and may be restarted by selecting it there.
|
||||
|
||||
The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend,
|
||||
:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum
|
||||
requirements of the `MPRIS specification <http://www.mpris.org/>`_. The
|
||||
``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
|
||||
|
||||
|
||||
Using a custom audio sink
|
||||
=========================
|
||||
|
||||
If you have successfully installed GStreamer, and then run the ``gst-inspect``
|
||||
or ``gst-inspect-0.10`` command, you should see a long listing of installed
|
||||
plugins, ending in a summary line::
|
||||
|
||||
$ gst-inspect-0.10
|
||||
... long list of installed plugins ...
|
||||
Total count: 254 plugins (1 blacklist entry not shown), 1156 features
|
||||
|
||||
Next, you should be able to produce a audible tone by running::
|
||||
|
||||
gst-launch-0.10 audiotestsrc ! audioresample ! autoaudiosink
|
||||
|
||||
If you cannot hear any sound when running this command, you won't hear any
|
||||
sound from Mopidy either, as Mopidy by default uses GStreamer's
|
||||
``autoaudiosink`` to play audio. Thus, make this work before you file a bug
|
||||
against Mopidy.
|
||||
|
||||
If you for some reason want to use some other GStreamer audio sink than
|
||||
``autoaudiosink``, you can set the setting :attr:`mopidy.settings.OUTPUT` to a
|
||||
partial GStreamer pipeline description describing the GStreamer sink you want
|
||||
to use.
|
||||
|
||||
Example of ``settings.py`` for using OSS4::
|
||||
|
||||
OUTPUT = u'oss4sink'
|
||||
|
||||
Again, this is the equivalent of the following ``gst-inspect`` command, so make
|
||||
this work first::
|
||||
|
||||
gst-launch-0.10 audiotestsrc ! audioresample ! oss4sink
|
||||
|
||||
|
||||
Streaming audio through a SHOUTcast/Icecast server
|
||||
==================================================
|
||||
|
||||
If you want to play the audio on another computer than the one running Mopidy,
|
||||
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
|
||||
streaming server. Multiple media players can then be connected to the streaming
|
||||
server simultaneously. To use the SHOUTcast output, do the following:
|
||||
|
||||
#. Install, configure and start the Icecast server. It can be found in the
|
||||
``icecast2`` package in Debian/Ubuntu.
|
||||
|
||||
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send``. An Ogg Vorbis
|
||||
encoder could be used instead of the lame MP3 encoder.
|
||||
|
||||
#. 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, to set the username and password, use:
|
||||
``lame ! shout2send username="foobar" password="s3cret"``.
|
||||
|
||||
Other advanced setups are also possible for outputs. Basically, anything you
|
||||
can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
:attr:`mopidy.settings.OUTPUT`.
|
||||
|
||||
|
||||
Custom settings
|
||||
===============
|
||||
|
||||
Mopidy's settings validator will stop you from defining any settings in your
|
||||
settings file that Mopidy doesn't know about. This may sound obnoxious, but it
|
||||
helps you detect typos in your settings, and deprecated settings that should be
|
||||
removed or updated.
|
||||
|
||||
If you're extending Mopidy in some way, and want to use Mopidy's settings
|
||||
system, you can prefix your settings with ``CUSTOM_`` to get around the
|
||||
settings validator. We recommend that you choose names like
|
||||
``CUSTOM_MYAPP_MYSETTING`` so that multiple custom extensions to Mopidy can be
|
||||
used at the same time without any danger of naming collisions.
|
||||
|
||||
|
||||
Available settings
|
||||
==================
|
||||
|
||||
.. automodule:: mopidy.settings
|
||||
:synopsis: Available settings and their default values
|
||||
:members:
|
||||
87
docs/troubleshooting.rst
Normal file
87
docs/troubleshooting.rst
Normal file
@ -0,0 +1,87 @@
|
||||
.. _troubleshooting:
|
||||
|
||||
***************
|
||||
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
|
||||
the `issue tracker <https://github.com/mopidy/mopidy/issues>`_.
|
||||
|
||||
When you're debugging yourself or asking for help, there are some tools built
|
||||
into Mopidy that you should know about.
|
||||
|
||||
|
||||
Sharing config and log output
|
||||
=============================
|
||||
|
||||
If you're getting help at IRC, we recommend that you use a pastebin, like
|
||||
`pastebin.com <http://pastebin.com/>`_ or `GitHub Gist
|
||||
<https://gist.github.com/>`_, to share your configuration and log output.
|
||||
Pasting more than a couple of lines on IRC is generally frowned upon. On the
|
||||
mailing list or when reporting an issue, somewhat longer text dumps are
|
||||
accepted, but large logs should still be shared through a pastebin.
|
||||
|
||||
|
||||
Effective configuration
|
||||
=======================
|
||||
|
||||
The command :option:`mopidy --show-config` will print your full effective
|
||||
configuration the way Mopidy sees it after all defaults and all config files
|
||||
have been merged into a single config document. Any secret values like
|
||||
passwords are masked out, so the output of the command should be safe to share
|
||||
with others for debugging.
|
||||
|
||||
|
||||
Installed dependencies
|
||||
======================
|
||||
|
||||
The command :option:`mopidy --show-deps` will list the paths to and versions of
|
||||
any dependency Mopidy or the extensions might need to work. This is very useful
|
||||
data for checking that you're using the right versions, and that you're using
|
||||
the right installation if you have multiple installations of a dependency on
|
||||
your system.
|
||||
|
||||
|
||||
Debug logging
|
||||
=============
|
||||
|
||||
If you run :option:`mopidy -v`, Mopidy will output debug log to stdout. If you
|
||||
run :option:`mopidy --save-debug-log`, it will save the debug log to the file
|
||||
``mopidy.log`` in the directory you ran the command from.
|
||||
|
||||
If you want to turn on more or less logging for some component, see the
|
||||
docs for the :confval:`loglevels/*` config section.
|
||||
|
||||
|
||||
Debugging deadlocks
|
||||
===================
|
||||
|
||||
If Mopidy hangs without and 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
|
||||
system is deadlocking. If you have the ``pkill`` command installed, you can use
|
||||
this by simply running::
|
||||
|
||||
pkill -SIGUSR1 mopidy
|
||||
|
||||
|
||||
Debugging GStreamer
|
||||
===================
|
||||
|
||||
If you really want to dig in and debug GStreamer behaviour, then check out the
|
||||
`Debugging section
|
||||
<http://gstreamer.freedesktop.org/data/doc/gstreamer/head/manual/html/section-checklist-debug.html>`_
|
||||
of GStreamer's documentation for your options. Note that Mopidy does not
|
||||
support the GStreamer command line options, like ``--gst-debug-level=3``, but
|
||||
setting GStreamer environment variables, like :envvar:`GST_DEBUG`, works with
|
||||
Mopidy. For example, to run Mopidy with debug logging and GStreamer logging at
|
||||
level 3, you can run::
|
||||
|
||||
GST_DEBUG=3 mopidy -v
|
||||
|
||||
This will produce a lot of output, but given some GStreamer knowledge this is
|
||||
very useful for debugging GStreamer pipeline issues.
|
||||
23
docs/versioning.rst
Normal file
23
docs/versioning.rst
Normal file
@ -0,0 +1,23 @@
|
||||
**********
|
||||
Versioning
|
||||
**********
|
||||
|
||||
Mopidy uses `Semantic Versioning <http://semver.org/>`_, but since we're still
|
||||
pre-1.0 that doesn't mean much yet.
|
||||
|
||||
|
||||
Release schedule
|
||||
================
|
||||
|
||||
We intend to have about one feature release every month in periods of active
|
||||
development. The feature releases are numbered 0.x.0. The features added is a
|
||||
mix of what we feel is most important/requested of the missing features, and
|
||||
features we develop just because we find them fun to make, even though they may
|
||||
be useful for very few users or for a limited use case.
|
||||
|
||||
Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs
|
||||
that are too serious to wait for the next feature release. We will only release
|
||||
bugfix releases for the last feature release. E.g. when 0.14.0 is released, we
|
||||
will no longer provide bugfix releases for the 0.13 series. In other words,
|
||||
there will be just a single supported release at any point in time. This is to
|
||||
not spread our limited resources too thin.
|
||||
@ -41,8 +41,8 @@ After npm completes, you can import Mopidy.js using ``require()``:
|
||||
Using the library
|
||||
-----------------
|
||||
|
||||
See Mopidy's [HTTP frontend
|
||||
documentation](http://docs.mopidy.com/en/latest/modules/frontends/http/).
|
||||
See Mopidy's [HTTP API
|
||||
documentation](http://docs.mopidy.com/en/latest/api/http/).
|
||||
|
||||
|
||||
Building from source
|
||||
|
||||
@ -15,17 +15,12 @@ if not (2, 6) <= sys.version_info < (3,):
|
||||
'.'.join(map(str, sys.version_info[:3])))
|
||||
|
||||
if (isinstance(pykka.__version__, basestring)
|
||||
and not SV('1.0') <= SV(pykka.__version__) < SV('2.0')):
|
||||
and not SV('1.1') <= SV(pykka.__version__) < SV('2.0')):
|
||||
sys.exit(
|
||||
'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
|
||||
'Mopidy requires Pykka >= 1.1, < 2, but found %s' % pykka.__version__)
|
||||
|
||||
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.13.0'
|
||||
|
||||
|
||||
from mopidy import settings as default_settings_module
|
||||
from mopidy.utils.settings import SettingsProxy
|
||||
settings = SettingsProxy(default_settings_module)
|
||||
__version__ = '0.14.0'
|
||||
|
||||
@ -12,18 +12,10 @@ gobject.threads_init()
|
||||
import pykka.debug
|
||||
|
||||
|
||||
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
|
||||
# processing by GStreamer. This needs to be done before GStreamer is imported,
|
||||
# so that GStreamer doesn't hijack e.g. ``--help``.
|
||||
# NOTE This naive fix does not support values like ``bar`` in
|
||||
# ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``.
|
||||
|
||||
def is_gst_arg(argument):
|
||||
return argument.startswith('--gst') or argument == '--help-gst'
|
||||
|
||||
gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)]
|
||||
mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)]
|
||||
sys.argv[1:] = gstreamer_args
|
||||
# Extract any command line arguments. This needs to be done before GStreamer is
|
||||
# imported, so that GStreamer doesn't hijack e.g. ``--help``.
|
||||
mopidy_args = sys.argv[1:]
|
||||
sys.argv[1:] = []
|
||||
|
||||
|
||||
# Add ../ to the path so we can run Mopidy from a Git checkout without
|
||||
@ -32,13 +24,11 @@ sys.path.insert(
|
||||
0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
|
||||
|
||||
|
||||
from mopidy import exceptions, settings
|
||||
from mopidy import ext
|
||||
from mopidy.audio import Audio
|
||||
from mopidy import config as config_lib
|
||||
from mopidy.core import Core
|
||||
from mopidy.utils import (
|
||||
deps, importing, log, path, process, settings as settings_utils,
|
||||
versioning)
|
||||
|
||||
from mopidy.utils import deps, log, path, process, versioning
|
||||
|
||||
logger = logging.getLogger('mopidy.main')
|
||||
|
||||
@ -49,44 +39,109 @@ def main():
|
||||
|
||||
loop = gobject.MainLoop()
|
||||
options = parse_options()
|
||||
config_files = options.config.split(b':')
|
||||
config_overrides = options.overrides
|
||||
|
||||
enabled_extensions = [] # Make sure it is defined before the finally block
|
||||
logging_initialized = False
|
||||
|
||||
# TODO: figure out a way to make the boilerplate in this file reusable in
|
||||
# scanner and other places we need it.
|
||||
|
||||
try:
|
||||
log.setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
check_old_folders()
|
||||
setup_settings(options.interactive)
|
||||
audio = setup_audio()
|
||||
backends = setup_backends(audio)
|
||||
# Initial config without extensions to bootstrap logging.
|
||||
logging_config, _ = config_lib.load(config_files, [], config_overrides)
|
||||
|
||||
# TODO: setup_logging needs defaults in-case config values are None
|
||||
log.setup_logging(
|
||||
logging_config, options.verbosity_level, options.save_debug_log)
|
||||
logging_initialized = True
|
||||
|
||||
installed_extensions = ext.load_extensions()
|
||||
|
||||
# TODO: wrap config in RO proxy.
|
||||
config, config_errors = config_lib.load(
|
||||
config_files, installed_extensions, config_overrides)
|
||||
|
||||
# Filter out disabled extensions and remove any config errors for them.
|
||||
for extension in installed_extensions:
|
||||
enabled = config[extension.ext_name]['enabled']
|
||||
if ext.validate_extension(extension) and enabled:
|
||||
enabled_extensions.append(extension)
|
||||
elif extension.ext_name in config_errors:
|
||||
del config_errors[extension.ext_name]
|
||||
|
||||
log_extension_info(installed_extensions, enabled_extensions)
|
||||
check_config_errors(config_errors)
|
||||
|
||||
# Read-only config from here on, please.
|
||||
proxied_config = config_lib.Proxy(config)
|
||||
|
||||
log.setup_log_levels(proxied_config)
|
||||
create_file_structures()
|
||||
check_old_locations()
|
||||
ext.register_gstreamer_elements(enabled_extensions)
|
||||
|
||||
# Anything that wants to exit after this point must use
|
||||
# mopidy.utils.process.exit_process as actors have been started.
|
||||
audio = setup_audio(proxied_config)
|
||||
backends = setup_backends(proxied_config, enabled_extensions, audio)
|
||||
core = setup_core(audio, backends)
|
||||
setup_frontends(core)
|
||||
setup_frontends(proxied_config, enabled_extensions, core)
|
||||
loop.run()
|
||||
except exceptions.SettingsError as ex:
|
||||
logger.error(ex.message)
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Interrupted. Exiting...')
|
||||
if logging_initialized:
|
||||
logger.info('Interrupted. Exiting...')
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
if logging_initialized:
|
||||
logger.exception(ex)
|
||||
raise
|
||||
finally:
|
||||
loop.quit()
|
||||
stop_frontends()
|
||||
stop_frontends(enabled_extensions)
|
||||
stop_core()
|
||||
stop_backends()
|
||||
stop_backends(enabled_extensions)
|
||||
stop_audio()
|
||||
process.stop_remaining_actors()
|
||||
|
||||
|
||||
def log_extension_info(all_extensions, enabled_extensions):
|
||||
# TODO: distinguish disabled vs blocked by env?
|
||||
enabled_names = set(e.ext_name for e in enabled_extensions)
|
||||
disabled_names = set(e.ext_name for e in all_extensions) - enabled_names
|
||||
logging.info(
|
||||
'Enabled extensions: %s', ', '.join(enabled_names) or 'none')
|
||||
logging.info(
|
||||
'Disabled extensions: %s', ', '.join(disabled_names) or 'none')
|
||||
|
||||
|
||||
def check_config_errors(errors):
|
||||
if not errors:
|
||||
return
|
||||
for section in errors:
|
||||
for key, msg in errors[section].items():
|
||||
logger.error('Config value %s/%s %s', section, key, msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_config_override(option, opt, override):
|
||||
try:
|
||||
return config_lib.parse_override(override)
|
||||
except ValueError:
|
||||
raise optparse.OptionValueError(
|
||||
'option %s: must have the format section/key=value' % opt)
|
||||
|
||||
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(
|
||||
version='Mopidy %s' % versioning.get_version())
|
||||
|
||||
# Ugly extension of optparse type checking magic :/
|
||||
optparse.Option.TYPES += ('config_override',)
|
||||
optparse.Option.TYPE_CHECKER['config_override'] = check_config_override
|
||||
|
||||
# NOTE First argument to add_option must be bytestrings on Python < 2.6.2
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
parser.add_option(
|
||||
b'--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
help='show GStreamer help options')
|
||||
parser.add_option(
|
||||
b'-i', '--interactive',
|
||||
action='store_true', dest='interactive',
|
||||
help='ask interactively for required settings which are missing')
|
||||
parser.add_option(
|
||||
b'-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
@ -100,89 +155,132 @@ def parse_options():
|
||||
action='store_true', dest='save_debug_log',
|
||||
help='save debug log to "./mopidy.log"')
|
||||
parser.add_option(
|
||||
b'--list-settings',
|
||||
action='callback',
|
||||
callback=settings_utils.list_settings_optparse_callback,
|
||||
help='list current settings')
|
||||
b'--show-config',
|
||||
action='callback', callback=show_config_callback,
|
||||
help='show current config')
|
||||
parser.add_option(
|
||||
b'--list-deps',
|
||||
action='callback', callback=deps.list_deps_optparse_callback,
|
||||
help='list dependencies and their versions')
|
||||
b'--show-deps',
|
||||
action='callback', callback=deps.show_deps_optparse_callback,
|
||||
help='show dependencies and their versions')
|
||||
parser.add_option(
|
||||
b'--debug-thread',
|
||||
action='store_true', dest='debug_thread',
|
||||
help='run background thread that dumps tracebacks on SIGUSR1')
|
||||
b'--config',
|
||||
action='store', dest='config',
|
||||
default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf',
|
||||
help='config files to use, colon seperated, later files override')
|
||||
parser.add_option(
|
||||
b'-o', b'--option',
|
||||
action='append', dest='overrides', type='config_override',
|
||||
help='`section/key=value` values to override config options')
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
|
||||
|
||||
def check_old_folders():
|
||||
old_settings_folder = os.path.expanduser('~/.mopidy')
|
||||
def show_config_callback(option, opt, value, parser):
|
||||
# TODO: don't use callback for this as --config or -o set after
|
||||
# --show-config will be ignored.
|
||||
files = getattr(parser.values, 'config', b'').split(b':')
|
||||
overrides = getattr(parser.values, 'overrides', [])
|
||||
|
||||
if not os.path.isdir(old_settings_folder):
|
||||
return
|
||||
extensions = ext.load_extensions()
|
||||
config, errors = config_lib.load(files, extensions, overrides)
|
||||
|
||||
logger.warning(
|
||||
'Old settings folder found at %s, settings.py should be moved '
|
||||
'to %s, any cache data should be deleted. See release notes for '
|
||||
'further instructions.', old_settings_folder, path.SETTINGS_PATH)
|
||||
# Clear out any config for disabled extensions.
|
||||
for extension in extensions:
|
||||
if not ext.validate_extension(extension):
|
||||
config[extension.ext_name] = {b'enabled': False}
|
||||
errors[extension.ext_name] = {
|
||||
b'enabled': b'extension disabled its self.'}
|
||||
elif not config[extension.ext_name]['enabled']:
|
||||
config[extension.ext_name] = {b'enabled': False}
|
||||
errors[extension.ext_name] = {
|
||||
b'enabled': b'extension disabled by config.'}
|
||||
|
||||
print config_lib.format(config, extensions, errors)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def setup_settings(interactive):
|
||||
path.get_or_create_folder(path.SETTINGS_PATH)
|
||||
path.get_or_create_folder(path.DATA_PATH)
|
||||
path.get_or_create_file(path.SETTINGS_FILE)
|
||||
try:
|
||||
settings.validate(interactive)
|
||||
except exceptions.SettingsError as ex:
|
||||
logger.error(ex.message)
|
||||
sys.exit(1)
|
||||
def check_old_locations():
|
||||
dot_mopidy_dir = path.expand_path(b'~/.mopidy')
|
||||
if os.path.isdir(dot_mopidy_dir):
|
||||
logger.warning(
|
||||
'Old Mopidy dot dir found at %s. Please migrate your config to '
|
||||
'the ini-file based config format. See release notes for further '
|
||||
'instructions.', dot_mopidy_dir)
|
||||
|
||||
old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
|
||||
if os.path.isfile(old_settings_file):
|
||||
logger.warning(
|
||||
'Old Mopidy settings file found at %s. Please migrate your '
|
||||
'config to the ini-file based config format. See release notes '
|
||||
'for further instructions.', old_settings_file)
|
||||
|
||||
|
||||
def setup_audio():
|
||||
return Audio.start().proxy()
|
||||
def create_file_structures():
|
||||
path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy')
|
||||
path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
|
||||
|
||||
|
||||
def setup_audio(config):
|
||||
logger.info('Starting Mopidy audio')
|
||||
return Audio.start(config=config).proxy()
|
||||
|
||||
|
||||
def stop_audio():
|
||||
logger.info('Stopping Mopidy audio')
|
||||
process.stop_actors_by_class(Audio)
|
||||
|
||||
|
||||
def setup_backends(audio):
|
||||
def setup_backends(config, extensions, audio):
|
||||
backend_classes = []
|
||||
for extension in extensions:
|
||||
backend_classes.extend(extension.get_backend_classes())
|
||||
|
||||
logger.info(
|
||||
'Starting Mopidy backends: %s',
|
||||
', '.join(b.__name__ for b in backend_classes) or 'none')
|
||||
|
||||
backends = []
|
||||
for backend_class_name in settings.BACKENDS:
|
||||
backend_class = importing.get_class(backend_class_name)
|
||||
backend = backend_class.start(audio=audio).proxy()
|
||||
for backend_class in backend_classes:
|
||||
backend = backend_class.start(config=config, audio=audio).proxy()
|
||||
backends.append(backend)
|
||||
|
||||
return backends
|
||||
|
||||
|
||||
def stop_backends():
|
||||
for backend_class_name in settings.BACKENDS:
|
||||
process.stop_actors_by_class(importing.get_class(backend_class_name))
|
||||
def stop_backends(extensions):
|
||||
logger.info('Stopping Mopidy backends')
|
||||
for extension in extensions:
|
||||
for backend_class in extension.get_backend_classes():
|
||||
process.stop_actors_by_class(backend_class)
|
||||
|
||||
|
||||
def setup_core(audio, backends):
|
||||
logger.info('Starting Mopidy core')
|
||||
return Core.start(audio=audio, backends=backends).proxy()
|
||||
|
||||
|
||||
def stop_core():
|
||||
logger.info('Stopping Mopidy core')
|
||||
process.stop_actors_by_class(Core)
|
||||
|
||||
|
||||
def setup_frontends(core):
|
||||
for frontend_class_name in settings.FRONTENDS:
|
||||
try:
|
||||
importing.get_class(frontend_class_name).start(core=core)
|
||||
except exceptions.OptionalDependencyError as ex:
|
||||
logger.info('Disabled: %s (%s)', frontend_class_name, ex)
|
||||
def setup_frontends(config, extensions, core):
|
||||
frontend_classes = []
|
||||
for extension in extensions:
|
||||
frontend_classes.extend(extension.get_frontend_classes())
|
||||
|
||||
logger.info(
|
||||
'Starting Mopidy frontends: %s',
|
||||
', '.join(f.__name__ for f in frontend_classes) or 'none')
|
||||
|
||||
for frontend_class in frontend_classes:
|
||||
frontend_class.start(config=config, core=core)
|
||||
|
||||
|
||||
def stop_frontends():
|
||||
for frontend_class_name in settings.FRONTENDS:
|
||||
try:
|
||||
frontend_class = importing.get_class(frontend_class_name)
|
||||
def stop_frontends(extensions):
|
||||
logger.info('Stopping Mopidy frontends')
|
||||
for extension in extensions:
|
||||
for frontend_class in extension.get_frontend_classes():
|
||||
process.stop_actors_by_class(frontend_class)
|
||||
except exceptions.OptionalDependencyError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -9,7 +9,6 @@ import logging
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.utils import process
|
||||
|
||||
from . import mixers, utils
|
||||
@ -27,20 +26,16 @@ MB = 1 << 20
|
||||
class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.OUTPUT`
|
||||
- :attr:`mopidy.settings.MIXER`
|
||||
- :attr:`mopidy.settings.MIXER_TRACK`
|
||||
"""
|
||||
|
||||
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
|
||||
state = PlaybackState.STOPPED
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config):
|
||||
super(Audio, self).__init__()
|
||||
|
||||
self._config = config
|
||||
|
||||
self._playbin = None
|
||||
self._signal_ids = {} # {(element, event): signal_id}
|
||||
|
||||
@ -143,47 +138,51 @@ class Audio(pykka.ThreadingActor):
|
||||
self._playbin.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_output(self):
|
||||
output_desc = self._config['audio']['output']
|
||||
try:
|
||||
output = gst.parse_bin_from_description(
|
||||
settings.OUTPUT, ghost_unconnected_pads=True)
|
||||
output_desc, ghost_unconnected_pads=True)
|
||||
self._playbin.set_property('audio-sink', output)
|
||||
logger.info('Audio output set to "%s"', settings.OUTPUT)
|
||||
logger.info('Audio output set to "%s"', output_desc)
|
||||
except gobject.GError as ex:
|
||||
logger.error(
|
||||
'Failed to create audio output "%s": %s', settings.OUTPUT, ex)
|
||||
'Failed to create audio output "%s": %s', output_desc, ex)
|
||||
process.exit_process()
|
||||
|
||||
def _setup_mixer(self):
|
||||
if not settings.MIXER:
|
||||
mixer_desc = self._config['audio']['mixer']
|
||||
track_desc = self._config['audio']['mixer_track']
|
||||
|
||||
if mixer_desc is None:
|
||||
logger.info('Not setting up audio mixer')
|
||||
return
|
||||
|
||||
if settings.MIXER == 'software':
|
||||
if mixer_desc == 'software':
|
||||
self._software_mixing = True
|
||||
logger.info('Audio mixer is using software mixing')
|
||||
return
|
||||
|
||||
try:
|
||||
mixerbin = gst.parse_bin_from_description(
|
||||
settings.MIXER, ghost_unconnected_pads=False)
|
||||
mixer_desc, ghost_unconnected_pads=False)
|
||||
except gobject.GError as ex:
|
||||
logger.warning(
|
||||
'Failed to create audio mixer "%s": %s', settings.MIXER, ex)
|
||||
'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"', settings.MIXER)
|
||||
'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', settings.MIXER)
|
||||
'Setting audio mixer "%s" to READY failed', mixer_desc)
|
||||
return
|
||||
|
||||
track = self._select_mixer_track(mixer, settings.MIXER_TRACK)
|
||||
track = self._select_mixer_track(mixer, track_desc)
|
||||
if not track:
|
||||
logger.warning('Could not find usable audio mixer track')
|
||||
return
|
||||
@ -198,8 +197,9 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
def _select_mixer_track(self, mixer, track_label):
|
||||
# Ignore tracks without volumes, then look for track with
|
||||
# label == settings.MIXER_TRACK, otherwise fallback to first usable
|
||||
# track hoping the mixer gave them to us in a sensible order.
|
||||
# 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():
|
||||
|
||||
@ -28,6 +28,9 @@ class AudioListener(object):
|
||||
*MAY* be implemented by actor. By default, this method forwards the
|
||||
event to the specific event methods.
|
||||
|
||||
For a list of what event names to expect, see the names of the other
|
||||
methods in :class:`AudioListener`.
|
||||
|
||||
:param event: the event name
|
||||
:type event: string
|
||||
:param kwargs: any other arguments to the specific event handlers
|
||||
|
||||
@ -7,7 +7,6 @@ import gobject
|
||||
|
||||
from .auto import AutoAudioMixer
|
||||
from .fake import FakeMixer
|
||||
from .nad import NadMixer
|
||||
|
||||
|
||||
def register_mixer(mixer_class):
|
||||
@ -19,4 +18,3 @@ def register_mixer(mixer_class):
|
||||
def register_mixers():
|
||||
register_mixer(AutoAudioMixer)
|
||||
register_mixer(FakeMixer)
|
||||
register_mixer(NadMixer)
|
||||
|
||||
@ -2,14 +2,18 @@
|
||||
|
||||
This is Mopidy's default mixer.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
Dependencies
|
||||
============
|
||||
|
||||
**Settings:**
|
||||
None
|
||||
|
||||
- If this wasn't the default, you would set :attr:`mopidy.settings.MIXER`
|
||||
to ``autoaudiomixer`` to use this mixer.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
If this wasn't the default, you would set the :confval:`audio/mixer` config
|
||||
value to ``autoaudiomixer`` to use this mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
"""Fake mixer for use in tests.
|
||||
|
||||
**Dependencies:**
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- None
|
||||
None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer.
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this
|
||||
mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
@ -1,289 +0,0 @@
|
||||
"""Mixer that controls volume using a NAD amplifier.
|
||||
|
||||
The NAD amplifier must be connected to the machine running Mopidy using a
|
||||
serial cable.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
.. literalinclude:: ../../../../requirements/external_mixers.txt
|
||||
|
||||
**Settings:**
|
||||
|
||||
- Set :attr:`mopidy.settings.MIXER` to ``nadmixer`` to use it. You probably
|
||||
also needs to add some properties to the ``MIXER`` setting.
|
||||
|
||||
Supported properties includes:
|
||||
|
||||
``port``:
|
||||
The serial device to use, defaults to ``/dev/ttyUSB0``. This must be
|
||||
set correctly for the mixer to work.
|
||||
|
||||
``source``:
|
||||
The source that should be selected on the amplifier, like ``aux``,
|
||||
``disc``, ``tape``, ``tuner``, etc. Leave unset if you don't want the
|
||||
mixer to change it for you.
|
||||
|
||||
``speakers-a``:
|
||||
Set to ``on`` or ``off`` if you want the mixer to make sure that
|
||||
speaker set A is turned on or off. Leave unset if you don't want the
|
||||
mixer to change it for you.
|
||||
|
||||
``speakers-b``:
|
||||
See ``speakers-a``.
|
||||
|
||||
Configuration examples::
|
||||
|
||||
# Minimum configuration, if the amplifier is available at /dev/ttyUSB0
|
||||
MIXER = u'nadmixer'
|
||||
|
||||
# Minimum configuration, if the amplifier is available elsewhere
|
||||
MIXER = u'nadmixer port=/dev/ttyUSB3'
|
||||
|
||||
# Full configuration
|
||||
MIXER = (
|
||||
u'nadmixer port=/dev/ttyUSB0 '
|
||||
u'source=aux speakers-a=on speakers-b=off')
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gobject
|
||||
import gst
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
serial = None # noqa
|
||||
|
||||
import pykka
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.audio.mixers.nad')
|
||||
|
||||
|
||||
class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
|
||||
__gstdetails__ = (
|
||||
'NadMixer',
|
||||
'Mixer',
|
||||
'Mixer to control NAD amplifiers using a serial link',
|
||||
'Mopidy')
|
||||
|
||||
port = gobject.property(type=str, default='/dev/ttyUSB0')
|
||||
source = gobject.property(type=str)
|
||||
speakers_a = gobject.property(type=str)
|
||||
speakers_b = gobject.property(type=str)
|
||||
|
||||
_volume_cache = 0
|
||||
_nad_talker = None
|
||||
|
||||
def list_tracks(self):
|
||||
track = utils.create_track(
|
||||
label='Master',
|
||||
initial_volume=0,
|
||||
min_volume=0,
|
||||
max_volume=100,
|
||||
num_channels=1,
|
||||
flags=(
|
||||
gst.interfaces.MIXER_TRACK_MASTER |
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT))
|
||||
return [track]
|
||||
|
||||
def get_volume(self, track):
|
||||
return [self._volume_cache]
|
||||
|
||||
def set_volume(self, track, volumes):
|
||||
if len(volumes):
|
||||
volume = volumes[0]
|
||||
self._volume_cache = volume
|
||||
self._nad_talker.set_volume(volume)
|
||||
|
||||
def set_mute(self, track, mute):
|
||||
self._nad_talker.mute(mute)
|
||||
|
||||
def do_change_state(self, transition):
|
||||
if transition == gst.STATE_CHANGE_NULL_TO_READY:
|
||||
if serial is None:
|
||||
logger.warning('nadmixer dependency python-serial not found')
|
||||
return gst.STATE_CHANGE_FAILURE
|
||||
self._start_nad_talker()
|
||||
return gst.STATE_CHANGE_SUCCESS
|
||||
|
||||
def _start_nad_talker(self):
|
||||
self._nad_talker = NadTalker.start(
|
||||
port=self.port,
|
||||
source=self.source or None,
|
||||
speakers_a=self.speakers_a or None,
|
||||
speakers_b=self.speakers_b or None
|
||||
).proxy()
|
||||
|
||||
|
||||
class NadTalker(pykka.ThreadingActor):
|
||||
"""
|
||||
Independent thread which does the communication with the NAD amplifier.
|
||||
|
||||
Since the communication is done in an independent thread, Mopidy won't
|
||||
block other requests while doing rather time consuming work like
|
||||
calibrating the NAD amplifier's volume.
|
||||
"""
|
||||
|
||||
# Serial link settings
|
||||
BAUDRATE = 115200
|
||||
BYTESIZE = 8
|
||||
PARITY = 'N'
|
||||
STOPBITS = 1
|
||||
|
||||
# Timeout in seconds used for read/write operations.
|
||||
# If you set the timeout too low, the reads will never get complete
|
||||
# confirmations and calibration will decrease volume forever. If you set
|
||||
# the timeout too high, stuff takes more time. 0.2s seems like a good value
|
||||
# for NAD C 355BEE.
|
||||
TIMEOUT = 0.2
|
||||
|
||||
# Number of volume levels the amplifier supports. 40 for NAD C 355BEE.
|
||||
VOLUME_LEVELS = 40
|
||||
|
||||
def __init__(self, port, source, speakers_a, speakers_b):
|
||||
super(NadTalker, self).__init__()
|
||||
|
||||
self.port = port
|
||||
self.source = source
|
||||
self.speakers_a = speakers_a
|
||||
self.speakers_b = speakers_b
|
||||
|
||||
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
|
||||
self._nad_volume = None
|
||||
|
||||
self._device = None
|
||||
|
||||
def on_start(self):
|
||||
self._open_connection()
|
||||
self._set_device_to_known_state()
|
||||
|
||||
def _open_connection(self):
|
||||
logger.info('NAD amplifier: Connecting through "%s"', self.port)
|
||||
self._device = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.BAUDRATE,
|
||||
bytesize=self.BYTESIZE,
|
||||
parity=self.PARITY,
|
||||
stopbits=self.STOPBITS,
|
||||
timeout=self.TIMEOUT)
|
||||
self._get_device_model()
|
||||
|
||||
def _set_device_to_known_state(self):
|
||||
self._power_device_on()
|
||||
self._select_speakers()
|
||||
self._select_input_source()
|
||||
self.mute(False)
|
||||
self.calibrate_volume()
|
||||
|
||||
def _get_device_model(self):
|
||||
model = self._ask_device('Main.Model')
|
||||
logger.info('NAD amplifier: Connected to model "%s"', model)
|
||||
return model
|
||||
|
||||
def _power_device_on(self):
|
||||
self._check_and_set('Main.Power', 'On')
|
||||
|
||||
def _select_speakers(self):
|
||||
if self.speakers_a is not None:
|
||||
self._check_and_set('Main.SpeakerA', self.speakers_a.title())
|
||||
if self.speakers_b is not None:
|
||||
self._check_and_set('Main.SpeakerB', self.speakers_b.title())
|
||||
|
||||
def _select_input_source(self):
|
||||
if self.source is not None:
|
||||
self._check_and_set('Main.Source', self.source.title())
|
||||
|
||||
def mute(self, mute):
|
||||
if mute:
|
||||
self._check_and_set('Main.Mute', 'On')
|
||||
else:
|
||||
self._check_and_set('Main.Mute', 'Off')
|
||||
|
||||
def calibrate_volume(self, current_nad_volume=None):
|
||||
# The NAD C 355BEE amplifier has 40 different volume levels. We have no
|
||||
# way of asking on which level we are. Thus, we must calibrate the
|
||||
# mixer by decreasing the volume 39 times.
|
||||
if current_nad_volume is None:
|
||||
current_nad_volume = self.VOLUME_LEVELS
|
||||
if current_nad_volume == self.VOLUME_LEVELS:
|
||||
logger.info('NAD amplifier: Calibrating by setting volume to 0')
|
||||
self._nad_volume = current_nad_volume
|
||||
if self._decrease_volume():
|
||||
current_nad_volume -= 1
|
||||
if current_nad_volume == 0:
|
||||
logger.info('NAD amplifier: Done calibrating')
|
||||
else:
|
||||
self.actor_ref.proxy().calibrate_volume(current_nad_volume)
|
||||
|
||||
def set_volume(self, volume):
|
||||
# Increase or decrease the amplifier volume until it matches the given
|
||||
# target volume.
|
||||
logger.debug('Setting volume to %d' % volume)
|
||||
target_nad_volume = int(round(volume * self.VOLUME_LEVELS / 100.0))
|
||||
if self._nad_volume is None:
|
||||
return # Calibration needed
|
||||
while target_nad_volume > self._nad_volume:
|
||||
if self._increase_volume():
|
||||
self._nad_volume += 1
|
||||
while target_nad_volume < self._nad_volume:
|
||||
if self._decrease_volume():
|
||||
self._nad_volume -= 1
|
||||
|
||||
def _increase_volume(self):
|
||||
# Increase volume. Returns :class:`True` if confirmed by device.
|
||||
self._write('Main.Volume+')
|
||||
return self._readline() == 'Main.Volume+'
|
||||
|
||||
def _decrease_volume(self):
|
||||
# Decrease volume. Returns :class:`True` if confirmed by device.
|
||||
self._write('Main.Volume-')
|
||||
return self._readline() == 'Main.Volume-'
|
||||
|
||||
def _check_and_set(self, key, value):
|
||||
for attempt in range(1, 4):
|
||||
if self._ask_device(key) == value:
|
||||
return
|
||||
logger.info(
|
||||
'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
|
||||
key, value, attempt)
|
||||
self._command_device(key, value)
|
||||
if self._ask_device(key) != value:
|
||||
logger.info(
|
||||
'NAD amplifier: Gave up on setting "%s" to "%s"',
|
||||
key, value)
|
||||
|
||||
def _ask_device(self, key):
|
||||
self._write('%s?' % key)
|
||||
return self._readline().replace('%s=' % key, '')
|
||||
|
||||
def _command_device(self, key, value):
|
||||
if type(value) == unicode:
|
||||
value = value.encode('utf-8')
|
||||
self._write('%s=%s' % (key, value))
|
||||
self._readline()
|
||||
|
||||
def _write(self, data):
|
||||
# Write data to device. Prepends and appends a newline to the data, as
|
||||
# recommended by the NAD documentation.
|
||||
if not self._device.isOpen():
|
||||
self._device.open()
|
||||
self._device.write('\n%s\n' % data)
|
||||
logger.debug('Write: %s', data)
|
||||
|
||||
def _readline(self):
|
||||
# Read line from device. The result is stripped for leading and
|
||||
# trailing whitespace.
|
||||
if not self._device.isOpen():
|
||||
self._device.open()
|
||||
result = self._device.readline().strip()
|
||||
if result:
|
||||
logger.debug('Read: %s', result)
|
||||
return result
|
||||
@ -11,17 +11,17 @@ class Backend(object):
|
||||
audio = None
|
||||
|
||||
#: The library provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseLibraryProvider`, or :class:`None` if
|
||||
#: :class:`~mopidy.backends.base.BaseLibraryProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide a library.
|
||||
library = None
|
||||
|
||||
#: The playback provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
|
||||
#: :class:`~mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide playback.
|
||||
playback = None
|
||||
|
||||
#: The playlists provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BasePlaylistsProvider`, or class:`None` if
|
||||
#: :class:`~mopidy.backends.base.BasePlaylistsProvider`, or class:`None` if
|
||||
#: the backend doesn't provide playlists.
|
||||
playlists = None
|
||||
|
||||
|
||||
@ -5,13 +5,13 @@ used in tests of the frontends.
|
||||
|
||||
The backend handles URIs starting with ``dummy:``.
|
||||
|
||||
**Dependencies:**
|
||||
**Dependencies**
|
||||
|
||||
- None
|
||||
None
|
||||
|
||||
**Settings:**
|
||||
**Default config**
|
||||
|
||||
- None
|
||||
None
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
@ -22,8 +22,12 @@ from mopidy.backends import base
|
||||
from mopidy.models import Playlist, SearchResult
|
||||
|
||||
|
||||
def create_dummy_backend_proxy(config=None, audio=None):
|
||||
return DummyBackend.start(config=config, audio=audio).proxy()
|
||||
|
||||
|
||||
class DummyBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, audio):
|
||||
def __init__(self, config, audio):
|
||||
super(DummyBackend, self).__init__()
|
||||
|
||||
self.library = DummyLibraryProvider(backend=self)
|
||||
|
||||
@ -1,26 +1,31 @@
|
||||
"""A backend for playing music from a local music archive.
|
||||
|
||||
This backend handles URIs starting with ``file:``.
|
||||
|
||||
See :ref:`music-from-local-storage` for further instructions on using this
|
||||
backend.
|
||||
|
||||
**Issues:**
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Local+backend
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
|
||||
- :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
|
||||
- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import LocalBackend
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-Local'
|
||||
ext_name = 'local'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['media_dir'] = config.Path()
|
||||
schema['playlists_dir'] = config.Path()
|
||||
schema['tag_cache_file'] = config.Path()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
pass
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .actor import LocalBackend
|
||||
return [LocalBackend]
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
from .library import LocalLibraryProvider
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
@ -13,11 +15,34 @@ logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, audio):
|
||||
def __init__(self, config, audio):
|
||||
super(LocalBackend, self).__init__()
|
||||
|
||||
self.config = config
|
||||
|
||||
self.check_dirs_and_files()
|
||||
|
||||
self.library = LocalLibraryProvider(backend=self)
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = ['file']
|
||||
|
||||
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']['playlists_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local playlists dir: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
try:
|
||||
path.get_or_create_file(self.config['local']['tag_cache_file'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create empty tag cache file: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
5
mopidy/backends/local/ext.conf
Normal file
5
mopidy/backends/local/ext.conf
Normal file
@ -0,0 +1,5 @@
|
||||
[local]
|
||||
enabled = true
|
||||
media_dir = $XDG_MUSIC_DIR
|
||||
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
|
||||
tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache
|
||||
@ -1,8 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Album, SearchResult
|
||||
|
||||
@ -15,19 +13,24 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._uri_mapping = {}
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._tag_cache_file = self.backend.config['local']['tag_cache_file']
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, uri=None):
|
||||
tracks = parse_mpd_tag_cache(
|
||||
settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH)
|
||||
logger.debug(
|
||||
'Loading local tracks from %s using %s',
|
||||
self._media_dir, self._tag_cache_file)
|
||||
|
||||
logger.info(
|
||||
'Loading tracks from %s using %s',
|
||||
settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE)
|
||||
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
|
||||
|
||||
for track in tracks:
|
||||
self._uri_mapping[track.uri] = track
|
||||
|
||||
logger.info(
|
||||
'Loaded %d local tracks from %s using %s',
|
||||
len(tracks), self._media_dir, self._tag_cache_file)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
return [self._uri_mapping[uri]]
|
||||
|
||||
@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base, listener
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.utils import formatting, path
|
||||
@ -19,7 +18,8 @@ logger = logging.getLogger('mopidy.backends.local')
|
||||
class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
self._path = settings.LOCAL_PLAYLIST_PATH
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._playlists_dir = self.backend.config['local']['playlists_dir']
|
||||
self.refresh()
|
||||
|
||||
def create(self, name):
|
||||
@ -42,16 +42,14 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
return playlist
|
||||
|
||||
def refresh(self):
|
||||
logger.info('Loading playlists from %s', self._path)
|
||||
|
||||
playlists = []
|
||||
|
||||
for m3u in glob.glob(os.path.join(self._path, '*.m3u')):
|
||||
for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')):
|
||||
uri = path.path_to_uri(m3u)
|
||||
name = os.path.splitext(os.path.basename(m3u))[0]
|
||||
|
||||
tracks = []
|
||||
for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH):
|
||||
for track_uri in parse_m3u(m3u, self._media_dir):
|
||||
try:
|
||||
# TODO We must use core.library.lookup() to support tracks
|
||||
# from other backends
|
||||
@ -65,6 +63,10 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
self.playlists = playlists
|
||||
listener.BackendListener.send('playlists_loaded')
|
||||
|
||||
logger.info(
|
||||
'Loaded %d local playlists from %s',
|
||||
len(playlists), self._playlists_dir)
|
||||
|
||||
def save(self, playlist):
|
||||
assert playlist.uri, 'Cannot save playlist without URI'
|
||||
|
||||
@ -86,13 +88,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
|
||||
def _get_m3u_path(self, name):
|
||||
name = formatting.slugify(name)
|
||||
file_path = os.path.join(self._path, name + '.m3u')
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._path)
|
||||
file_path = os.path.join(self._playlists_dir, name + '.m3u')
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
return file_path
|
||||
|
||||
def _save_m3u(self, playlist):
|
||||
file_path = path.uri_to_path(playlist.uri)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._path)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
with open(file_path, 'w') as file_handle:
|
||||
for track in playlist.tracks:
|
||||
if track.uri.startswith('file://'):
|
||||
@ -103,16 +105,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
|
||||
def _delete_m3u(self, uri):
|
||||
file_path = path.uri_to_path(uri)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._path)
|
||||
path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
def _rename_m3u(self, playlist):
|
||||
src_file_path = path.uri_to_path(playlist.uri)
|
||||
path.check_file_path_is_inside_base_dir(src_file_path, self._path)
|
||||
path.check_file_path_is_inside_base_dir(
|
||||
src_file_path, self._playlists_dir)
|
||||
|
||||
dst_file_path = self._get_m3u_path(playlist.name)
|
||||
path.check_file_path_is_inside_base_dir(dst_file_path, self._path)
|
||||
path.check_file_path_is_inside_base_dir(
|
||||
dst_file_path, self._playlists_dir)
|
||||
|
||||
shutil.move(src_file_path, dst_file_path)
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from mopidy.utils.path import path_to_uri
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
def parse_m3u(file_path, music_folder):
|
||||
def parse_m3u(file_path, media_dir):
|
||||
r"""
|
||||
Convert M3U file list of uris
|
||||
|
||||
@ -31,6 +31,7 @@ def parse_m3u(file_path, music_folder):
|
||||
- This function does not bother with Extended M3U directives.
|
||||
"""
|
||||
|
||||
# TODO: uris as bytes
|
||||
uris = []
|
||||
try:
|
||||
with open(file_path) as m3u:
|
||||
@ -49,7 +50,7 @@ def parse_m3u(file_path, music_folder):
|
||||
if line.startswith('file://'):
|
||||
uris.append(line)
|
||||
else:
|
||||
path = path_to_uri(music_folder, line)
|
||||
path = path_to_uri(media_dir, line)
|
||||
uris.append(path)
|
||||
|
||||
return uris
|
||||
@ -71,6 +72,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
||||
current = {}
|
||||
state = None
|
||||
|
||||
# TODO: uris as bytes
|
||||
for line in contents.split(b'\n'):
|
||||
if line == b'songList begin':
|
||||
state = 'songs'
|
||||
|
||||
@ -1,35 +1,36 @@
|
||||
"""A backend for playing music from Spotify
|
||||
|
||||
`Spotify <http://www.spotify.com/>`_ is a music streaming service. The backend
|
||||
uses the official `libspotify
|
||||
<http://developer.spotify.com/en/libspotify/overview/>`_ library and the
|
||||
`pyspotify <http://github.com/mopidy/pyspotify/>`_ Python bindings for
|
||||
libspotify. This backend handles URIs starting with ``spotify:``.
|
||||
|
||||
See :ref:`music-from-spotify` for further instructions on using this backend.
|
||||
|
||||
.. note::
|
||||
|
||||
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
|
||||
otherwise approved in any way by Spotify. Spotify is the registered
|
||||
trade mark of the Spotify Group.
|
||||
|
||||
**Issues:**
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
.. literalinclude:: ../../../requirements/spotify.txt
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
|
||||
- :attr:`mopidy.settings.SPOTIFY_USERNAME`
|
||||
- :attr:`mopidy.settings.SPOTIFY_PASSWORD`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import SpotifyBackend
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, exceptions, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-Spotify'
|
||||
ext_name = 'spotify'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['username'] = config.String()
|
||||
schema['password'] = config.Secret()
|
||||
schema['bitrate'] = config.Integer(choices=(96, 160, 320))
|
||||
schema['timeout'] = config.Integer(minimum=0)
|
||||
schema['cache_dir'] = config.Path()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
try:
|
||||
import spotify # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('pyspotify library not found', e)
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .actor import SpotifyBackend
|
||||
return [SpotifyBackend]
|
||||
|
||||
@ -4,23 +4,20 @@ import logging
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.backends.spotify.library import SpotifyLibraryProvider
|
||||
from mopidy.backends.spotify.playback import SpotifyPlaybackProvider
|
||||
from mopidy.backends.spotify.session_manager import SpotifySessionManager
|
||||
from mopidy.backends.spotify.playlists import SpotifyPlaylistsProvider
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
|
||||
class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
||||
# Imports inside methods are to prevent loading of __init__.py to fail on
|
||||
# missing spotify dependencies.
|
||||
|
||||
def __init__(self, audio):
|
||||
def __init__(self, config, audio):
|
||||
super(SpotifyBackend, self).__init__()
|
||||
|
||||
from .library import SpotifyLibraryProvider
|
||||
from .playback import SpotifyPlaybackProvider
|
||||
from .session_manager import SpotifySessionManager
|
||||
from .playlists import SpotifyPlaylistsProvider
|
||||
self.config = config
|
||||
|
||||
self.library = SpotifyLibraryProvider(backend=self)
|
||||
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
|
||||
@ -28,17 +25,8 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
||||
|
||||
self.uri_schemes = ['spotify']
|
||||
|
||||
# Fail early if settings are not present
|
||||
username = settings.SPOTIFY_USERNAME
|
||||
password = settings.SPOTIFY_PASSWORD
|
||||
proxy = settings.SPOTIFY_PROXY_HOST
|
||||
proxy_username = settings.SPOTIFY_PROXY_USERNAME
|
||||
proxy_password = settings.SPOTIFY_PROXY_PASSWORD
|
||||
|
||||
self.spotify = SpotifySessionManager(
|
||||
username, password, audio=audio, backend_ref=self.actor_ref,
|
||||
proxy=proxy, proxy_username=proxy_username,
|
||||
proxy_password=proxy_password)
|
||||
config, audio=audio, backend_ref=self.actor_ref)
|
||||
|
||||
def on_start(self):
|
||||
logger.info('Mopidy uses SPOTIFY(R) CORE')
|
||||
|
||||
7
mopidy/backends/spotify/ext.conf
Normal file
7
mopidy/backends/spotify/ext.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[spotify]
|
||||
enabled = true
|
||||
username =
|
||||
password =
|
||||
bitrate = 160
|
||||
timeout = 10
|
||||
cache_dir = $XDG_CACHE_DIR/mopidy/spotify
|
||||
@ -7,7 +7,6 @@ import urllib
|
||||
import pykka
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Track, SearchResult
|
||||
|
||||
@ -62,6 +61,10 @@ class SpotifyTrack(Track):
|
||||
|
||||
|
||||
class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SpotifyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._timeout = self.backend.config['spotify']['timeout']
|
||||
|
||||
def find_exact(self, query=None, uris=None):
|
||||
return self.search(query=query, uris=uris)
|
||||
|
||||
@ -116,10 +119,11 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
SpotifyTrack(track=t)
|
||||
for t in playlist if t.availability() == TRACK_AVAILABLE]
|
||||
|
||||
def _wait_for_object_to_load(
|
||||
self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT):
|
||||
def _wait_for_object_to_load(self, spotify_obj, timeout=None):
|
||||
# XXX Sleeping to wait for the Spotify object to load is an ugly hack,
|
||||
# but it works. We should look into other solutions for this.
|
||||
if timeout is None:
|
||||
timeout = self._timeout
|
||||
wait_until = time.time() + timeout
|
||||
while not spotify_obj.is_loaded():
|
||||
time.sleep(0.1)
|
||||
@ -166,7 +170,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
future.set(search_result)
|
||||
|
||||
# Wait always returns None on python 2.6 :/
|
||||
self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT)
|
||||
self.backend.spotify.connected.wait(self._timeout)
|
||||
if not self.backend.spotify.connected.is_set():
|
||||
logger.debug('Not connected: Spotify search cancelled')
|
||||
return SearchResult(uri='spotify:search')
|
||||
@ -176,11 +180,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
album_count=200, artist_count=200, track_count=200)
|
||||
|
||||
try:
|
||||
return future.get(timeout=settings.SPOTIFY_TIMEOUT)
|
||||
return future.get(timeout=self._timeout)
|
||||
except pykka.Timeout:
|
||||
logger.debug(
|
||||
'Timeout: Spotify search did not return in %ds',
|
||||
settings.SPOTIFY_TIMEOUT)
|
||||
'Timeout: Spotify search did not return in %ds', self._timeout)
|
||||
return SearchResult(uri='spotify:search')
|
||||
|
||||
def _get_all_tracks(self):
|
||||
|
||||
@ -6,7 +6,7 @@ import threading
|
||||
|
||||
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
|
||||
|
||||
from mopidy import audio, settings
|
||||
from mopidy import audio
|
||||
from mopidy.backends.listener import BackendListener
|
||||
from mopidy.utils import process, versioning
|
||||
|
||||
@ -23,17 +23,22 @@ BITRATES = {96: 2, 160: 0, 320: 1}
|
||||
|
||||
|
||||
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
cache_location = settings.SPOTIFY_CACHE_PATH
|
||||
settings_location = cache_location
|
||||
cache_location = None
|
||||
settings_location = None
|
||||
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
||||
user_agent = 'Mopidy %s' % versioning.get_version()
|
||||
|
||||
def __init__(self, username, password, audio, backend_ref, proxy=None,
|
||||
proxy_username=None, proxy_password=None):
|
||||
def __init__(self, config, audio, backend_ref):
|
||||
|
||||
self.cache_location = config['spotify']['cache_dir']
|
||||
self.settings_location = config['spotify']['cache_dir']
|
||||
|
||||
PyspotifySessionManager.__init__(
|
||||
self, username, password, proxy=proxy,
|
||||
proxy_username=proxy_username,
|
||||
proxy_password=proxy_password)
|
||||
self, config['spotify']['username'], config['spotify']['password'],
|
||||
proxy=config['proxy']['hostname'],
|
||||
proxy_username=config['proxy']['username'],
|
||||
proxy_password=config['proxy']['password'])
|
||||
|
||||
process.BaseThread.__init__(self)
|
||||
self.name = 'SpotifyThread'
|
||||
|
||||
@ -41,6 +46,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
self.backend = None
|
||||
self.backend_ref = backend_ref
|
||||
|
||||
self.bitrate = config['spotify']['bitrate']
|
||||
|
||||
self.connected = threading.Event()
|
||||
self.push_audio_data = True
|
||||
self.buffer_timestamp = 0
|
||||
@ -66,10 +73,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
if not hasattr(self, 'session'):
|
||||
self.session = session
|
||||
|
||||
logger.debug(
|
||||
'Preferred Spotify bitrate is %s kbps',
|
||||
settings.SPOTIFY_BITRATE)
|
||||
session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
||||
logger.debug('Preferred Spotify bitrate is %d kbps', self.bitrate)
|
||||
session.set_preferred_bitrate(BITRATES[self.bitrate])
|
||||
|
||||
self.container_manager = SpotifyContainerManager(self)
|
||||
self.playlist_manager = SpotifyPlaylistManager(self)
|
||||
@ -167,11 +172,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
if not self._initial_data_receive_completed:
|
||||
logger.debug('Still getting data; skipped refresh of playlists')
|
||||
return
|
||||
playlists = map(
|
||||
translator.to_mopidy_playlist, self.session.playlist_container())
|
||||
playlists = []
|
||||
for spotify_playlist in self.session.playlist_container():
|
||||
playlists.append(translator.to_mopidy_playlist(
|
||||
spotify_playlist,
|
||||
bitrate=self.bitrate, username=self.username))
|
||||
playlists.append(translator.to_mopidy_playlist(
|
||||
self.session.starred(),
|
||||
bitrate=self.bitrate, username=self.username))
|
||||
playlists = filter(None, playlists)
|
||||
self.backend.playlists.playlists = playlists
|
||||
logger.info('Loaded %d Spotify playlist(s)', len(playlists))
|
||||
logger.info('Loaded %d Spotify playlists', len(playlists))
|
||||
BackendListener.send('playlists_loaded')
|
||||
|
||||
def logout(self):
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from spotify import Link
|
||||
import logging
|
||||
|
||||
import spotify
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
|
||||
artist_cache = {}
|
||||
album_cache = {}
|
||||
@ -14,7 +17,7 @@ track_cache = {}
|
||||
def to_mopidy_artist(spotify_artist):
|
||||
if spotify_artist is None:
|
||||
return
|
||||
uri = str(Link.from_artist(spotify_artist))
|
||||
uri = str(spotify.Link.from_artist(spotify_artist))
|
||||
if uri in artist_cache:
|
||||
return artist_cache[uri]
|
||||
if not spotify_artist.is_loaded():
|
||||
@ -26,7 +29,7 @@ def to_mopidy_artist(spotify_artist):
|
||||
def to_mopidy_album(spotify_album):
|
||||
if spotify_album is None:
|
||||
return
|
||||
uri = str(Link.from_album(spotify_album))
|
||||
uri = str(spotify.Link.from_album(spotify_album))
|
||||
if uri in album_cache:
|
||||
return album_cache[uri]
|
||||
if not spotify_album.is_loaded():
|
||||
@ -39,10 +42,10 @@ def to_mopidy_album(spotify_album):
|
||||
return album_cache[uri]
|
||||
|
||||
|
||||
def to_mopidy_track(spotify_track):
|
||||
def to_mopidy_track(spotify_track, bitrate=None):
|
||||
if spotify_track is None:
|
||||
return
|
||||
uri = str(Link.from_track(spotify_track, 0))
|
||||
uri = str(spotify.Link.from_track(spotify_track, 0))
|
||||
if uri in track_cache:
|
||||
return track_cache[uri]
|
||||
if not spotify_track.is_loaded():
|
||||
@ -60,27 +63,31 @@ def to_mopidy_track(spotify_track):
|
||||
track_no=spotify_track.index(),
|
||||
date=date,
|
||||
length=spotify_track.duration(),
|
||||
bitrate=settings.SPOTIFY_BITRATE)
|
||||
bitrate=bitrate)
|
||||
return track_cache[uri]
|
||||
|
||||
|
||||
def to_mopidy_playlist(spotify_playlist):
|
||||
def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None):
|
||||
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
|
||||
return
|
||||
uri = str(Link.from_playlist(spotify_playlist))
|
||||
try:
|
||||
uri = str(spotify.Link.from_playlist(spotify_playlist))
|
||||
except spotify.SpotifyError as e:
|
||||
logger.debug('Spotify playlist translation error: %s', e)
|
||||
return
|
||||
if not spotify_playlist.is_loaded():
|
||||
return Playlist(uri=uri, name='[loading...]')
|
||||
name = spotify_playlist.name()
|
||||
tracks = [
|
||||
to_mopidy_track(spotify_track, bitrate=bitrate)
|
||||
for spotify_track in spotify_playlist
|
||||
if not spotify_track.is_local()
|
||||
]
|
||||
if not name:
|
||||
# Other user's "starred" playlists isn't handled properly by pyspotify
|
||||
# See https://github.com/mopidy/pyspotify/issues/81
|
||||
return
|
||||
if spotify_playlist.owner().canonical_name() != settings.SPOTIFY_USERNAME:
|
||||
name = 'Starred'
|
||||
# Tracks in the Starred playlist are in reverse order from the official
|
||||
# client.
|
||||
tracks.reverse()
|
||||
if spotify_playlist.owner().canonical_name() != username:
|
||||
name += ' by ' + spotify_playlist.owner().canonical_name()
|
||||
return Playlist(
|
||||
uri=uri,
|
||||
name=name,
|
||||
tracks=[
|
||||
to_mopidy_track(spotify_track)
|
||||
for spotify_track in spotify_playlist
|
||||
if not spotify_track.is_local()])
|
||||
return Playlist(uri=uri, name=name, tracks=tracks)
|
||||
|
||||
@ -1,23 +1,29 @@
|
||||
"""A backend for playing music for streaming music.
|
||||
|
||||
This backend will handle streaming of URIs in
|
||||
:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are
|
||||
installed.
|
||||
|
||||
**Issues:**
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Stream+backend
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.STREAM_PROTOCOLS`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import StreamBackend
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-Stream'
|
||||
ext_name = 'stream'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['protocols'] = config.List()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
pass
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .actor import StreamBackend
|
||||
return [StreamBackend]
|
||||
|
||||
@ -5,7 +5,7 @@ import urlparse
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import audio as audio_lib, settings
|
||||
from mopidy import audio as audio_lib
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Track
|
||||
|
||||
@ -13,7 +13,7 @@ logger = logging.getLogger('mopidy.backends.stream')
|
||||
|
||||
|
||||
class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, audio):
|
||||
def __init__(self, config, audio):
|
||||
super(StreamBackend, self).__init__()
|
||||
|
||||
self.library = StreamLibraryProvider(backend=self)
|
||||
@ -21,7 +21,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||
self.playlists = None
|
||||
|
||||
self.uri_schemes = audio_lib.supported_uri_schemes(
|
||||
settings.STREAM_PROTOCOLS)
|
||||
config['stream']['protocols'])
|
||||
|
||||
|
||||
# TODO: Should we consider letting lookup know how to expand common playlist
|
||||
|
||||
9
mopidy/backends/stream/ext.conf
Normal file
9
mopidy/backends/stream/ext.conf
Normal file
@ -0,0 +1,9 @@
|
||||
[stream]
|
||||
enabled = true
|
||||
protocols =
|
||||
http
|
||||
https
|
||||
mms
|
||||
rtmp
|
||||
rtmps
|
||||
rtsp
|
||||
166
mopidy/config/__init__.py
Normal file
166
mopidy/config/__init__.py
Normal file
@ -0,0 +1,166 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import ConfigParser as configparser
|
||||
import io
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from mopidy.config.schemas import *
|
||||
from mopidy.config.types import *
|
||||
from mopidy.utils import path
|
||||
|
||||
logger = logging.getLogger('mopidy.config')
|
||||
|
||||
_logging_schema = ConfigSchema('logging')
|
||||
_logging_schema['console_format'] = String()
|
||||
_logging_schema['debug_format'] = String()
|
||||
_logging_schema['debug_file'] = Path()
|
||||
_logging_schema['config_file'] = Path(optional=True)
|
||||
|
||||
_loglevels_schema = LogLevelConfigSchema('loglevels')
|
||||
|
||||
_audio_schema = ConfigSchema('audio')
|
||||
_audio_schema['mixer'] = String()
|
||||
_audio_schema['mixer_track'] = String(optional=True)
|
||||
_audio_schema['output'] = String()
|
||||
|
||||
_proxy_schema = ConfigSchema('proxy')
|
||||
_proxy_schema['hostname'] = Hostname(optional=True)
|
||||
_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()
|
||||
|
||||
_schemas = [_logging_schema, _loglevels_schema, _audio_schema, _proxy_schema]
|
||||
|
||||
|
||||
def read(config_file):
|
||||
"""Helper to load config defaults in same way across core and extensions"""
|
||||
with io.open(config_file, 'rb') as filehandle:
|
||||
return filehandle.read()
|
||||
|
||||
|
||||
def load(files, extensions, overrides):
|
||||
# Helper to get configs, as the rest of our config system should not need
|
||||
# to know about extensions.
|
||||
config_dir = os.path.dirname(__file__)
|
||||
defaults = [read(os.path.join(config_dir, 'default.conf'))]
|
||||
defaults.extend(e.get_default_config() for e in extensions)
|
||||
raw_config = _load(files, defaults, overrides)
|
||||
|
||||
schemas = _schemas[:]
|
||||
schemas.extend(e.get_config_schema() for e in extensions)
|
||||
return _validate(raw_config, schemas)
|
||||
|
||||
|
||||
def format(config, extensions, comments=None, display=True):
|
||||
# Helper to format configs, as the rest of our config system should not
|
||||
# need to know about extensions.
|
||||
schemas = _schemas[:]
|
||||
schemas.extend(e.get_config_schema() for e in extensions)
|
||||
return _format(config, comments or {}, schemas, display)
|
||||
|
||||
|
||||
def _load(files, defaults, overrides):
|
||||
parser = configparser.RawConfigParser()
|
||||
|
||||
files = [path.expand_path(f) for f in files]
|
||||
sources = ['builtin defaults'] + files + ['command line options']
|
||||
logger.info('Loading config from: %s', ', '.join(sources))
|
||||
|
||||
# TODO: simply return path to config file for defaults so we can load it
|
||||
# all in the same way?
|
||||
for default in defaults:
|
||||
if isinstance(default, unicode):
|
||||
default = default.encode('utf-8')
|
||||
parser.readfp(io.BytesIO(default))
|
||||
|
||||
# Load config from a series of config files
|
||||
for filename in files:
|
||||
try:
|
||||
with io.open(filename, 'rb') as filehandle:
|
||||
parser.readfp(filehandle)
|
||||
except configparser.MissingSectionHeaderError as e:
|
||||
logging.warning('%s does not have a config section, not loaded.',
|
||||
filename)
|
||||
except configparser.ParsingError as e:
|
||||
linenos = ', '.join(str(lineno) for lineno, line in e.errors)
|
||||
logger.warning('%s has errors, line %s has been ignored.',
|
||||
filename, linenos)
|
||||
except IOError:
|
||||
# TODO: if this is the initial load of logging config we might not
|
||||
# have a logger at this point, we might want to handle this better.
|
||||
logger.debug('Config file %s not found; skipping', filename)
|
||||
|
||||
# If there have been parse errors there is a python bug that causes the
|
||||
# values to be lists, this little trick coerces these into strings.
|
||||
parser.readfp(io.BytesIO())
|
||||
|
||||
raw_config = {}
|
||||
for section in parser.sections():
|
||||
raw_config[section] = dict(parser.items(section))
|
||||
|
||||
for section, key, value in overrides or []:
|
||||
raw_config.setdefault(section, {})[key] = value
|
||||
|
||||
return raw_config
|
||||
|
||||
|
||||
def _validate(raw_config, schemas):
|
||||
# Get validated config
|
||||
config = {}
|
||||
errors = {}
|
||||
for schema in schemas:
|
||||
values = raw_config.get(schema.name, {})
|
||||
result, error = schema.deserialize(values)
|
||||
if error:
|
||||
errors[schema.name] = error
|
||||
if result:
|
||||
config[schema.name] = result
|
||||
return config, errors
|
||||
|
||||
|
||||
def _format(config, comments, schemas, display):
|
||||
output = []
|
||||
for schema in schemas:
|
||||
serialized = schema.serialize(config.get(schema.name, {}), display=display)
|
||||
if not serialized:
|
||||
continue
|
||||
output.append(b'[%s]' % bytes(schema.name))
|
||||
for key, value in serialized.items():
|
||||
comment = bytes(comments.get(schema.name, {}).get(key, ''))
|
||||
output.append(b'%s =' % bytes(key))
|
||||
if value is not None:
|
||||
output[-1] += b' ' + value
|
||||
if comment:
|
||||
output[-1] += b' # ' + comment.capitalize()
|
||||
output.append(b'')
|
||||
return b'\n'.join(output)
|
||||
|
||||
|
||||
def parse_override(override):
|
||||
"""Parse ``section/key=value`` command line overrides"""
|
||||
section, remainder = override.split('/', 1)
|
||||
key, value = remainder.split('=', 1)
|
||||
return (section.strip(), key.strip(), value.strip())
|
||||
|
||||
|
||||
class Proxy(collections.Mapping):
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
def __getitem__(self, key):
|
||||
item = self._data.__getitem__(key)
|
||||
if isinstance(item, dict):
|
||||
return Proxy(item)
|
||||
return item
|
||||
|
||||
def __iter__(self):
|
||||
return self._data.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return self._data.__len__()
|
||||
|
||||
def __repr__(self):
|
||||
return b'Proxy(%r)' % self._data
|
||||
125
mopidy/config/convert.py
Normal file
125
mopidy/config/convert.py
Normal file
@ -0,0 +1,125 @@
|
||||
from __future__ import 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('$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/output', 'OUTPUT')
|
||||
|
||||
helper('proxy/hostname', 'SPOTIFY_PROXY_HOST')
|
||||
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('local/tag_cache_file', 'LOCAL_TAG_CACHE_FILE')
|
||||
|
||||
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('$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,
|
||||
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.'
|
||||
18
mopidy/config/default.conf
Normal file
18
mopidy/config/default.conf
Normal file
@ -0,0 +1,18 @@
|
||||
[logging]
|
||||
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
|
||||
config_file =
|
||||
|
||||
[loglevels]
|
||||
pykka = info
|
||||
|
||||
[audio]
|
||||
mixer = autoaudiomixer
|
||||
mixer_track =
|
||||
output = autoaudiosink
|
||||
|
||||
[proxy]
|
||||
hostname =
|
||||
username =
|
||||
password =
|
||||
134
mopidy/config/schemas.py
Normal file
134
mopidy/config/schemas.py
Normal file
@ -0,0 +1,134 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
|
||||
from mopidy.config import types
|
||||
|
||||
# TODO: 2.6 cleanup (#344).
|
||||
ordered_dict = getattr(collections, 'OrderedDict', dict)
|
||||
|
||||
|
||||
def _did_you_mean(name, choices):
|
||||
"""Suggest most likely setting based on levenshtein."""
|
||||
if not choices:
|
||||
return None
|
||||
|
||||
name = name.lower()
|
||||
candidates = [(_levenshtein(name, c), c) for c in choices]
|
||||
candidates.sort()
|
||||
|
||||
if candidates[0][0] <= 3:
|
||||
return candidates[0][1]
|
||||
return None
|
||||
|
||||
|
||||
def _levenshtein(a, b):
|
||||
"""Calculates the Levenshtein distance between a and b."""
|
||||
n, m = len(a), len(b)
|
||||
if n > m:
|
||||
return _levenshtein(b, a)
|
||||
|
||||
current = xrange(n + 1)
|
||||
for i in xrange(1, m + 1):
|
||||
previous, current = current, [i] + [0] * n
|
||||
for j in xrange(1, n + 1):
|
||||
add, delete = previous[j] + 1, current[j - 1] + 1
|
||||
change = previous[j - 1]
|
||||
if a[j - 1] != b[i - 1]:
|
||||
change += 1
|
||||
current[j] = min(add, delete, change)
|
||||
return current[n]
|
||||
|
||||
|
||||
class ConfigSchema(object):
|
||||
"""Logical group of config values that correspond to a config section.
|
||||
|
||||
Schemas are set up by assigning config keys with config values to
|
||||
instances. Once setup :meth:`deserialize` can be called with a dict of
|
||||
values to process. For convienience we also support :meth:`format` method
|
||||
that can used for converting the values to a dict that can be printed and
|
||||
:meth:`serialize` for converting the values to a form suitable for
|
||||
persistence.
|
||||
"""
|
||||
# TODO: Use collections.OrderedDict once 2.6 support is gone (#344)
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self._schema = {}
|
||||
self._order = []
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self._schema:
|
||||
self._order.append(key)
|
||||
self._schema[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._schema[key]
|
||||
|
||||
def deserialize(self, values):
|
||||
"""Validates the given ``values`` using the config schema.
|
||||
|
||||
Returns a tuple with cleaned values and errors."""
|
||||
errors = {}
|
||||
result = {}
|
||||
|
||||
for key, value in values.items():
|
||||
try:
|
||||
result[key] = self._schema[key].deserialize(value)
|
||||
except KeyError: # not in our schema
|
||||
errors[key] = 'unknown config key.'
|
||||
suggestion = _did_you_mean(key, self._schema.keys())
|
||||
if suggestion:
|
||||
errors[key] += ' Did you mean %s?' % suggestion
|
||||
except ValueError as e: # deserialization failed
|
||||
result[key] = None
|
||||
errors[key] = str(e)
|
||||
|
||||
for key in self._schema:
|
||||
if key not in result and key not in errors:
|
||||
result[key] = None
|
||||
errors[key] = 'config key not found.'
|
||||
|
||||
return result, errors
|
||||
|
||||
def serialize(self, values, display=False):
|
||||
"""Converts the given ``values`` to a format suitable for persistence.
|
||||
|
||||
If ``display`` is :class:`True` secret config values, like passwords,
|
||||
will be masked out.
|
||||
|
||||
Returns a dict of config keys and values."""
|
||||
result = ordered_dict() # TODO: 2.6 cleanup (#344).
|
||||
for key in self._order:
|
||||
if key in values:
|
||||
result[key] = self._schema[key].serialize(values[key], display)
|
||||
return result
|
||||
|
||||
|
||||
class LogLevelConfigSchema(object):
|
||||
"""Special cased schema for handling a config section with loglevels.
|
||||
|
||||
Expects the config keys to be logger names and the values to be log levels
|
||||
as understood by the :class:`LogLevel` config value. Does not sub-class
|
||||
:class:`ConfigSchema`, but implements the same interface.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self._config_value = types.LogLevel()
|
||||
|
||||
def deserialize(self, values):
|
||||
errors = {}
|
||||
result = {}
|
||||
|
||||
for key, value in values.items():
|
||||
try:
|
||||
result[key] = self._config_value.deserialize(value)
|
||||
except ValueError as e: # deserialization failed
|
||||
result[key] = None
|
||||
errors[key] = str(e)
|
||||
return result, errors
|
||||
|
||||
def serialize(self, values, display=False):
|
||||
result = ordered_dict() # TODO: 2.6 cleanup (#344)
|
||||
for key in sorted(values.keys()):
|
||||
result[key] = self._config_value.serialize(values[key], display)
|
||||
return result
|
||||
259
mopidy/config/types.py
Normal file
259
mopidy/config/types.py
Normal file
@ -0,0 +1,259 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
|
||||
from mopidy.utils import path
|
||||
from mopidy.config import validators
|
||||
|
||||
|
||||
def decode(value):
|
||||
if isinstance(value, unicode):
|
||||
return value
|
||||
# TODO: only unescape \n \t and \\?
|
||||
return value.decode('string-escape').decode('utf-8')
|
||||
|
||||
|
||||
def encode(value):
|
||||
if not isinstance(value, unicode):
|
||||
return value
|
||||
for char in ('\\', '\n', '\t'): # TODO: more escapes?
|
||||
value = value.replace(char, char.encode('unicode-escape'))
|
||||
return value.encode('utf-8')
|
||||
|
||||
|
||||
class ExpandedPath(bytes):
|
||||
def __new__(self, original, expanded):
|
||||
return super(ExpandedPath, self).__new__(self, expanded)
|
||||
|
||||
def __init__(self, original, expanded):
|
||||
self.original = original
|
||||
|
||||
|
||||
class ConfigValue(object):
|
||||
"""Represents a config key's value and how to handle it.
|
||||
|
||||
Normally you will only be interacting with sub-classes for config values
|
||||
that encode either deserialization behavior and/or validation.
|
||||
|
||||
Each config value should be used for the following actions:
|
||||
|
||||
1. Deserializing from a raw string and validating, raising ValueError on
|
||||
failure.
|
||||
2. Serializing a value back to a string that can be stored in a config.
|
||||
3. Formatting a value to a printable form (useful for masking secrets).
|
||||
|
||||
:class:`None` values should not be deserialized, serialized or formatted,
|
||||
the code interacting with the config should simply skip None config values.
|
||||
"""
|
||||
|
||||
def deserialize(self, value):
|
||||
"""Cast raw string to appropriate type."""
|
||||
return value
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
"""Convert value back to string for saving."""
|
||||
if value is None:
|
||||
return b''
|
||||
return bytes(value)
|
||||
|
||||
|
||||
class String(ConfigValue):
|
||||
"""String value.
|
||||
|
||||
Is decoded as utf-8 and \\n \\t escapes should work and be preserved.
|
||||
"""
|
||||
def __init__(self, optional=False, choices=None):
|
||||
self._required = not optional
|
||||
self._choices = choices
|
||||
|
||||
def deserialize(self, value):
|
||||
value = decode(value).strip()
|
||||
validators.validate_required(value, self._required)
|
||||
validators.validate_choice(value, self._choices)
|
||||
if not value:
|
||||
return None
|
||||
return value
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if value is None:
|
||||
return b''
|
||||
return encode(value)
|
||||
|
||||
|
||||
class Secret(ConfigValue):
|
||||
"""Secret value.
|
||||
|
||||
Should be used for passwords, auth tokens etc. Deserializing will not
|
||||
convert to unicode. Will mask value when being displayed.
|
||||
"""
|
||||
def __init__(self, optional=False, choices=None):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value):
|
||||
value = value.strip()
|
||||
validators.validate_required(value, self._required)
|
||||
if not value:
|
||||
return None
|
||||
return value
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode('utf-8')
|
||||
if value is None:
|
||||
return b''
|
||||
elif display:
|
||||
return b'********'
|
||||
return value
|
||||
|
||||
|
||||
class Integer(ConfigValue):
|
||||
"""Integer value."""
|
||||
|
||||
def __init__(self, minimum=None, maximum=None, choices=None):
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._choices = choices
|
||||
|
||||
def deserialize(self, value):
|
||||
value = int(value)
|
||||
validators.validate_choice(value, self._choices)
|
||||
validators.validate_minimum(value, self._minimum)
|
||||
validators.validate_maximum(value, self._maximum)
|
||||
return value
|
||||
|
||||
|
||||
class Boolean(ConfigValue):
|
||||
"""Boolean value.
|
||||
|
||||
Accepts ``1``, ``yes``, ``true``, and ``on`` with any casing as
|
||||
:class:`True`.
|
||||
|
||||
Accepts ``0``, ``no``, ``false``, and ``off`` with any casing as
|
||||
:class:`False`.
|
||||
"""
|
||||
true_values = ('1', 'yes', 'true', 'on')
|
||||
false_values = ('0', 'no', 'false', 'off')
|
||||
|
||||
def deserialize(self, value):
|
||||
if value.lower() in self.true_values:
|
||||
return True
|
||||
elif value.lower() in self.false_values:
|
||||
return False
|
||||
raise ValueError('invalid value for boolean: %r' % value)
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if value:
|
||||
return b'true'
|
||||
else:
|
||||
return b'false'
|
||||
|
||||
|
||||
class List(ConfigValue):
|
||||
"""List value.
|
||||
|
||||
Supports elements split by commas or newlines. Newlines take presedence and
|
||||
empty list items will be filtered out.
|
||||
"""
|
||||
def __init__(self, optional=False):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value):
|
||||
if b'\n' in value:
|
||||
values = re.split(r'\s*\n\s*', value)
|
||||
else:
|
||||
values = re.split(r'\s*,\s*', value)
|
||||
values = (decode(v).strip() for v in values)
|
||||
values = filter(None, values)
|
||||
validators.validate_required(values, self._required)
|
||||
return tuple(values)
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
return b'\n ' + b'\n '.join(encode(v) for v in value if v)
|
||||
|
||||
|
||||
class LogLevel(ConfigValue):
|
||||
"""Log level value.
|
||||
|
||||
Expects one of ``critical``, ``error``, ``warning``, ``info``, ``debug``
|
||||
with any casing.
|
||||
"""
|
||||
levels = {
|
||||
b'critical': logging.CRITICAL,
|
||||
b'error': logging.ERROR,
|
||||
b'warning': logging.WARNING,
|
||||
b'info': logging.INFO,
|
||||
b'debug': logging.DEBUG,
|
||||
}
|
||||
|
||||
def deserialize(self, value):
|
||||
validators.validate_choice(value.lower(), self.levels.keys())
|
||||
return self.levels.get(value.lower())
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
lookup = dict((v, k) for k, v in self.levels.items())
|
||||
if value in lookup:
|
||||
return lookup[value]
|
||||
return b''
|
||||
|
||||
|
||||
class Hostname(ConfigValue):
|
||||
"""Network hostname value."""
|
||||
|
||||
def __init__(self, optional=False):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value, display=False):
|
||||
validators.validate_required(value, self._required)
|
||||
if not value.strip():
|
||||
return None
|
||||
try:
|
||||
socket.getaddrinfo(value, None)
|
||||
except socket.error:
|
||||
raise ValueError('must be a resolveable hostname or valid IP')
|
||||
return value
|
||||
|
||||
|
||||
class Port(Integer):
|
||||
"""Network port value.
|
||||
|
||||
Expects integer in the range 0-65535, zero tells the kernel to simply
|
||||
allocate a port for us.
|
||||
"""
|
||||
# TODO: consider probing if port is free or not?
|
||||
def __init__(self, choices=None):
|
||||
super(Port, self).__init__(minimum=0, maximum=2**16-1, choices=choices)
|
||||
|
||||
|
||||
class Path(ConfigValue):
|
||||
"""File system path
|
||||
|
||||
The following expansions of the path will be done:
|
||||
|
||||
- ``~`` to the current user's home directory
|
||||
|
||||
- ``$XDG_CACHE_DIR`` according to the XDG spec
|
||||
|
||||
- ``$XDG_CONFIG_DIR`` according to the XDG spec
|
||||
|
||||
- ``$XDG_DATA_DIR`` according to the XDG spec
|
||||
|
||||
- ``$XDG_MUSIC_DIR`` according to the XDG spec
|
||||
"""
|
||||
def __init__(self, optional=False):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value):
|
||||
value = value.strip()
|
||||
expanded = path.expand_path(value)
|
||||
validators.validate_required(value, self._required)
|
||||
validators.validate_required(expanded, self._required)
|
||||
if not value or expanded is None:
|
||||
return None
|
||||
return ExpandedPath(value, expanded)
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if isinstance(value, ExpandedPath):
|
||||
return value.original
|
||||
return value
|
||||
41
mopidy/config/validators.py
Normal file
41
mopidy/config/validators.py
Normal file
@ -0,0 +1,41 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# TODO: add validate regexp?
|
||||
|
||||
|
||||
def validate_required(value, required):
|
||||
"""Validate that ``value`` is set if ``required``
|
||||
|
||||
Normally called in :meth:`~mopidy.config.types.ConfigValue.deserialize` on
|
||||
the raw string, _not_ the converted value.
|
||||
"""
|
||||
if required and not value:
|
||||
raise ValueError('must be set.')
|
||||
|
||||
|
||||
def validate_choice(value, choices):
|
||||
"""Validate that ``value`` is one of the ``choices``
|
||||
|
||||
Normally called in :meth:`~mopidy.config.types.ConfigValue.deserialize`.
|
||||
"""
|
||||
if choices is not None and value not in choices:
|
||||
names = ', '.join(repr(c) for c in choices)
|
||||
raise ValueError('must be one of %s, not %s.' % (names, value))
|
||||
|
||||
|
||||
def validate_minimum(value, minimum):
|
||||
"""Validate that ``value`` is at least ``minimum``
|
||||
|
||||
Normally called in :meth:`~mopidy.config.types.ConfigValue.deserialize`.
|
||||
"""
|
||||
if minimum is not None and value < minimum:
|
||||
raise ValueError('%r must be larger than %r.' % (value, minimum))
|
||||
|
||||
|
||||
def validate_maximum(value, maximum):
|
||||
"""Validate that ``value`` is at most ``maximum``
|
||||
|
||||
Normally called in :meth:`~mopidy.config.types.ConfigValue.deserialize`.
|
||||
"""
|
||||
if maximum is not None and value > maximum:
|
||||
raise ValueError('%r must be smaller than %r.' % (value, maximum))
|
||||
@ -16,9 +16,5 @@ class MopidyException(Exception):
|
||||
self._message = message
|
||||
|
||||
|
||||
class SettingsError(MopidyException):
|
||||
pass
|
||||
|
||||
|
||||
class OptionalDependencyError(MopidyException):
|
||||
class ExtensionError(MopidyException):
|
||||
pass
|
||||
|
||||
164
mopidy/ext.py
Normal file
164
mopidy/ext.py
Normal file
@ -0,0 +1,164 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import pkg_resources
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy import config as config_lib
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.ext')
|
||||
|
||||
|
||||
class Extension(object):
|
||||
"""Base class for Mopidy extensions"""
|
||||
|
||||
dist_name = None
|
||||
"""The extension's distribution name, as registered on PyPI
|
||||
|
||||
Example: ``Mopidy-Soundspot``
|
||||
"""
|
||||
|
||||
ext_name = None
|
||||
"""The extension's short name, as used in setup.py and as config section
|
||||
name
|
||||
|
||||
Example: ``soundspot``
|
||||
"""
|
||||
|
||||
version = None
|
||||
"""The extension's version
|
||||
|
||||
Should match the :attr:`__version__` attribute on the extension's main
|
||||
Python module and the version registered on PyPI.
|
||||
"""
|
||||
|
||||
def get_default_config(self):
|
||||
"""The extension's default config as a bytestring
|
||||
|
||||
:returns: bytes or unicode
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Add at least a config section with "enabled = true"')
|
||||
|
||||
def get_config_schema(self):
|
||||
"""The extension's config validation schema
|
||||
|
||||
:returns: :class:`~mopidy.config.schema.ExtensionConfigSchema`
|
||||
"""
|
||||
schema = config_lib.ConfigSchema(self.ext_name)
|
||||
schema['enabled'] = config_lib.Boolean()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
"""Checks if the extension can run in the current environment
|
||||
|
||||
For example, this method can be used to check if all dependencies that
|
||||
are needed are installed.
|
||||
|
||||
:raises: :class:`~mopidy.exceptions.ExtensionError`
|
||||
:returns: :class:`None`
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_frontend_classes(self):
|
||||
"""List of frontend actor classes
|
||||
|
||||
Mopidy will take care of starting the actors.
|
||||
|
||||
:returns: list of :class:`pykka.Actor` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_backend_classes(self):
|
||||
"""List of backend actor classes
|
||||
|
||||
Mopidy will take care of starting the actors.
|
||||
|
||||
:returns: list of :class:`~mopidy.backends.base.Backend` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
"""Hook for registering custom GStreamer elements
|
||||
|
||||
Register custom GStreamer elements by implementing this method.
|
||||
Example::
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
from .mixer import SoundspotMixer
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||
|
||||
:returns: :class:`None`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def load_extensions():
|
||||
"""Find all installed extensions.
|
||||
|
||||
:returns: list of installed extensions
|
||||
"""
|
||||
|
||||
installed_extensions = []
|
||||
|
||||
for entry_point in pkg_resources.iter_entry_points('mopidy.ext'):
|
||||
logger.debug('Loading entry point: %s', entry_point)
|
||||
extension_class = entry_point.load(require=False)
|
||||
extension = extension_class()
|
||||
extension.entry_point = entry_point
|
||||
installed_extensions.append(extension)
|
||||
logger.debug(
|
||||
'Loaded extension: %s %s', extension.dist_name, extension.version)
|
||||
|
||||
names = (e.ext_name for e in installed_extensions)
|
||||
logging.debug('Discovered extensions: %s', ', '.join(names))
|
||||
return installed_extensions
|
||||
|
||||
|
||||
def validate_extension(extension):
|
||||
"""Verify extension's dependencies and environment.
|
||||
|
||||
:param extensions: an extension to check
|
||||
:returns: if extension should be run
|
||||
"""
|
||||
|
||||
logger.debug('Validating extension: %s', extension.ext_name)
|
||||
|
||||
if extension.ext_name != extension.entry_point.name:
|
||||
logger.warning(
|
||||
'Disabled extension %(ep)s: entry point name (%(ep)s) '
|
||||
'does not match extension name (%(ext)s)',
|
||||
{'ep': extension.entry_point.name, 'ext': extension.ext_name})
|
||||
return False
|
||||
|
||||
try:
|
||||
extension.entry_point.require()
|
||||
except pkg_resources.DistributionNotFound as ex:
|
||||
logger.info(
|
||||
'Disabled extension %s: Dependency %s not found',
|
||||
extension.ext_name, ex)
|
||||
return False
|
||||
|
||||
try:
|
||||
extension.validate_environment()
|
||||
except exceptions.ExtensionError as ex:
|
||||
logger.info(
|
||||
'Disabled extension %s: %s', extension.ext_name, ex.message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register_gstreamer_elements(enabled_extensions):
|
||||
"""Registers custom GStreamer elements from extensions.
|
||||
|
||||
:params enabled_extensions: list of enabled extensions
|
||||
"""
|
||||
|
||||
for extension in enabled_extensions:
|
||||
logger.debug(
|
||||
'Registering GStreamer elements for: %s', extension.ext_name)
|
||||
extension.register_gstreamer_elements()
|
||||
@ -1,481 +1,39 @@
|
||||
"""
|
||||
The HTTP frontends lets you control Mopidy through HTTP and WebSockets, e.g.
|
||||
from a web based client.
|
||||
from __future__ import unicode_literals
|
||||
|
||||
**Dependencies**
|
||||
import os
|
||||
|
||||
.. literalinclude:: ../../../requirements/http.txt
|
||||
import mopidy
|
||||
from mopidy import config, exceptions, ext
|
||||
|
||||
**Settings**
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`
|
||||
class Extension(ext.Extension):
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_PORT`
|
||||
dist_name = 'Mopidy-HTTP'
|
||||
ext_name = 'http'
|
||||
version = mopidy.__version__
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR`
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['hostname'] = config.Hostname()
|
||||
schema['port'] = config.Port()
|
||||
schema['static_dir'] = config.Path(optional=True)
|
||||
return schema
|
||||
|
||||
Setup
|
||||
=====
|
||||
def validate_environment(self):
|
||||
try:
|
||||
import cherrypy # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('cherrypy library not found', e)
|
||||
|
||||
When this frontend is included in :attr:`mopidy.settings.FRONTENDS`, it starts
|
||||
a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`.
|
||||
try:
|
||||
import ws4py # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('ws4py library not found', e)
|
||||
|
||||
.. warning:: Security
|
||||
|
||||
As a simple security measure, the web server is by default only available
|
||||
from localhost. To make it available from other computers, change
|
||||
:attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that
|
||||
the HTTP frontend does not feature any form of user authentication or
|
||||
authorization. Anyone able to access the web server can use the full core
|
||||
API of Mopidy. Thus, you probably only want to make the web server
|
||||
available from your local network or place it behind a web proxy which
|
||||
takes care or user authentication. You have been warned.
|
||||
|
||||
|
||||
Using a web based Mopidy client
|
||||
===============================
|
||||
|
||||
The web server can also host any static files, for example the HTML, CSS,
|
||||
JavaScript, and images needed for a web based Mopidy client. To host static
|
||||
files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the
|
||||
root directory of your web client, e.g.::
|
||||
|
||||
HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client'
|
||||
|
||||
If the directory includes a file named ``index.html``, it will be served on the
|
||||
root of Mopidy's web server.
|
||||
|
||||
If you're making a web based client and wants to do server side development as
|
||||
well, you are of course free to run your own web server and just use Mopidy's
|
||||
web server for the APIs. But, for clients implemented purely in JavaScript,
|
||||
letting Mopidy host the files is a simpler solution.
|
||||
|
||||
|
||||
WebSocket API
|
||||
=============
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Since this frontend 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Event messages
|
||||
--------------
|
||||
|
||||
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 can be recognized by checking for the key named
|
||||
``jsonrpc`` with the string value ``2.0``. For details on the messaging format,
|
||||
please refer to the `JSON-RPC 2.0 spec
|
||||
<http://www.jsonrpc.org/specification>`_.
|
||||
|
||||
All methods (not attributes) in the :ref:`core-api` is made available through
|
||||
JSON-RPC calls over the WebSocket. For example,
|
||||
:meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method
|
||||
``core.playback.play``.
|
||||
|
||||
The core API's attributes is made available through setters and getters. For
|
||||
example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is
|
||||
available as the JSON-RPC method ``core.playback.get_current_track``.
|
||||
|
||||
Example JSON-RPC request::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_current_track"}
|
||||
|
||||
Example JSON-RPC response::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", "...": "..."}}
|
||||
|
||||
The JSON-RPC method ``core.describe`` returns a data structure describing all
|
||||
available methods. If you're unsure how the core API maps to JSON-RPC, having a
|
||||
look at the ``core.describe`` response can be helpful.
|
||||
|
||||
|
||||
Mopidy.js JavaScript library
|
||||
============================
|
||||
|
||||
We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets
|
||||
you quickly started with working on your client instead of figuring out how to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting the library for browser use
|
||||
-----------------------------------
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
Thus, if you use Mopidy to host your web client, like described above, you can
|
||||
load the latest version of Mopidy.js by adding the following script tag to your
|
||||
HTML file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
If you don't use Mopidy to host your web client, you can find the JS files in
|
||||
the Git repo at:
|
||||
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
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").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. Create an empty directory for your web client.
|
||||
|
||||
2. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point
|
||||
to your new directory.
|
||||
|
||||
3. Make sure that you've included
|
||||
``mopidy.frontends.http.HttpFrontend`` in
|
||||
:attr:`mopidy.settings.FRONTENDS`.
|
||||
|
||||
4. Start/restart Mopidy.
|
||||
|
||||
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
|
||||
world!".
|
||||
|
||||
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
|
||||
there.
|
||||
|
||||
7. Include Mopidy.js in your web page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
8. Add one of the following Mopidy.js examples of how to queue and start
|
||||
playback of your first playlist either to your web page or a JavaScript file
|
||||
that you include in your web page.
|
||||
|
||||
"Imperative" style:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var consoleError = console.error.bind(error);
|
||||
|
||||
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(error);
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import HttpFrontend
|
||||
def get_frontend_classes(self):
|
||||
from .actor import HttpFrontend
|
||||
return [HttpFrontend]
|
||||
|
||||
@ -4,18 +4,13 @@ import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
import cherrypy
|
||||
import pykka
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
|
||||
from mopidy import exceptions, models, settings
|
||||
from mopidy import models
|
||||
from mopidy.core import CoreListener
|
||||
|
||||
try:
|
||||
import cherrypy
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
except ImportError as import_error:
|
||||
raise exceptions.OptionalDependencyError(import_error)
|
||||
|
||||
from . import ws
|
||||
|
||||
|
||||
@ -23,8 +18,9 @@ logger = logging.getLogger('mopidy.frontends.http')
|
||||
|
||||
|
||||
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
def __init__(self, config, core):
|
||||
super(HttpFrontend, self).__init__()
|
||||
self.config = config
|
||||
self.core = core
|
||||
self._setup_server()
|
||||
self._setup_websocket_plugin()
|
||||
@ -34,9 +30,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def _setup_server(self):
|
||||
cherrypy.config.update({
|
||||
'engine.autoreload_on': False,
|
||||
'server.socket_host': (
|
||||
settings.HTTP_SERVER_HOSTNAME.encode('utf-8')),
|
||||
'server.socket_port': settings.HTTP_SERVER_PORT,
|
||||
'server.socket_host': self.config['http']['hostname'],
|
||||
'server.socket_port': self.config['http']['port'],
|
||||
})
|
||||
|
||||
def _setup_websocket_plugin(self):
|
||||
@ -48,8 +43,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
root.mopidy = MopidyResource()
|
||||
root.mopidy.ws = ws.WebSocketResource(self.core)
|
||||
|
||||
if settings.HTTP_SERVER_STATIC_DIR:
|
||||
static_dir = settings.HTTP_SERVER_STATIC_DIR
|
||||
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)
|
||||
|
||||
8
mopidy/frontends/http/ext.conf
Normal file
8
mopidy/frontends/http/ext.conf
Normal file
@ -0,0 +1,8 @@
|
||||
[http]
|
||||
enabled = true
|
||||
hostname = 127.0.0.1
|
||||
port = 6680
|
||||
static_dir =
|
||||
|
||||
[loglevels]
|
||||
cherrypy = warning
|
||||
@ -2,14 +2,11 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy import core, exceptions, models
|
||||
from mopidy.utils import jsonrpc
|
||||
import cherrypy
|
||||
from ws4py.websocket import WebSocket
|
||||
|
||||
try:
|
||||
import cherrypy
|
||||
from ws4py.websocket import WebSocket
|
||||
except ImportError as import_error:
|
||||
raise exceptions.OptionalDependencyError(import_error)
|
||||
from mopidy import core, models
|
||||
from mopidy.utils import jsonrpc
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.http')
|
||||
|
||||
@ -1,50 +1,33 @@
|
||||
"""The MPD server frontend.
|
||||
|
||||
MPD stands for Music Player Daemon. MPD is an independent project and server.
|
||||
Mopidy implements the MPD protocol, and is thus compatible with clients for the
|
||||
original MPD server.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PORT`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
|
||||
|
||||
**Usage:**
|
||||
|
||||
Make sure :attr:`mopidy.settings.FRONTENDS` includes
|
||||
``mopidy.frontends.mpd.MpdFrontend``. By default, the setting includes the MPD
|
||||
frontend.
|
||||
|
||||
**Limitations:**
|
||||
|
||||
This is a non exhaustive list of MPD features that Mopidy doesn't support.
|
||||
Items on this list will probably not be supported in the near future.
|
||||
|
||||
- Toggling of audio outputs is not supported
|
||||
- Channels for client-to-client communication are not supported
|
||||
- Stickers are not supported
|
||||
- Crossfade is not supported
|
||||
- Replay gain is not supported
|
||||
- ``count`` does not provide any statistics
|
||||
- ``stats`` does not provide any statistics
|
||||
- ``list`` does not support listing tracks by genre
|
||||
- ``decoders`` does not provide information about available decoders
|
||||
|
||||
The following items are currently not supported, but should be added in the
|
||||
near future:
|
||||
|
||||
- Modifying stored playlists is not supported
|
||||
- ``tagtypes`` is not supported
|
||||
- Browsing the file system is not supported
|
||||
- Live update of the music database is not supported
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import MpdFrontend
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-MPD'
|
||||
ext_name = 'mpd'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['hostname'] = config.Hostname()
|
||||
schema['port'] = config.Port()
|
||||
schema['password'] = config.Secret(optional=True)
|
||||
schema['max_connections'] = config.Integer(minimum=1)
|
||||
schema['connection_timeout'] = config.Integer(minimum=1)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
pass
|
||||
|
||||
def get_frontend_classes(self):
|
||||
from .actor import MpdFrontend
|
||||
return [MpdFrontend]
|
||||
|
||||
@ -5,7 +5,6 @@ import sys
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.frontends.mpd import session
|
||||
from mopidy.utils import encoding, network, process
|
||||
@ -14,19 +13,23 @@ logger = logging.getLogger('mopidy.frontends.mpd')
|
||||
|
||||
|
||||
class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
def __init__(self, config, core):
|
||||
super(MpdFrontend, self).__init__()
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
hostname = network.format_hostname(config['mpd']['hostname'])
|
||||
port = config['mpd']['port']
|
||||
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
try:
|
||||
network.Server(
|
||||
hostname, port,
|
||||
protocol=session.MpdSession, protocol_kwargs={b'core': core},
|
||||
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS,
|
||||
timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT)
|
||||
protocol=session.MpdSession,
|
||||
protocol_kwargs={
|
||||
b'config': config,
|
||||
b'core': core,
|
||||
},
|
||||
max_connections=config['mpd']['max_connections'],
|
||||
timeout=config['mpd']['connection_timeout'])
|
||||
except IOError as error:
|
||||
logger.error(
|
||||
'MPD server startup failed: %s',
|
||||
|
||||
@ -5,7 +5,6 @@ import re
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd import exceptions, protocol
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
|
||||
@ -22,13 +21,15 @@ class MpdDispatcher(object):
|
||||
|
||||
_noidle = re.compile(r'^noidle$')
|
||||
|
||||
def __init__(self, session=None, core=None):
|
||||
def __init__(self, session=None, config=None, core=None):
|
||||
self.config = config
|
||||
self.authenticated = False
|
||||
self.command_list_receiving = False
|
||||
self.command_list_ok = False
|
||||
self.command_list = []
|
||||
self.command_list_index = None
|
||||
self.context = MpdContext(self, session=session, core=core)
|
||||
self.context = MpdContext(
|
||||
self, session=session, config=config, core=core)
|
||||
|
||||
def handle_request(self, request, current_command_list_index=None):
|
||||
"""Dispatch incoming requests to the correct handler."""
|
||||
@ -82,7 +83,7 @@ class MpdDispatcher(object):
|
||||
def _authenticate_filter(self, request, response, filter_chain):
|
||||
if self.authenticated:
|
||||
return self._call_next_filter(request, response, filter_chain)
|
||||
elif settings.MPD_SERVER_PASSWORD is None:
|
||||
elif self.config['mpd']['password'] is None:
|
||||
self.authenticated = True
|
||||
return self._call_next_filter(request, response, filter_chain)
|
||||
else:
|
||||
@ -223,6 +224,9 @@ class MpdContext(object):
|
||||
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
|
||||
session = None
|
||||
|
||||
#: The Mopidy configuration.
|
||||
config = None
|
||||
|
||||
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
|
||||
core = None
|
||||
|
||||
@ -232,9 +236,55 @@ class MpdContext(object):
|
||||
#: The subsytems that we want to be notified about in idle mode.
|
||||
subscriptions = None
|
||||
|
||||
def __init__(self, dispatcher, session=None, core=None):
|
||||
def __init__(self, dispatcher, session=None, config=None, core=None):
|
||||
self.dispatcher = dispatcher
|
||||
self.session = session
|
||||
self.config = config
|
||||
self.core = core
|
||||
self.events = set()
|
||||
self.subscriptions = set()
|
||||
self._playlist_uri_from_name = {}
|
||||
self._playlist_name_from_uri = {}
|
||||
self.refresh_playlists_mapping()
|
||||
|
||||
def create_unique_name(self, playlist_name):
|
||||
name = playlist_name
|
||||
i = 2
|
||||
while name in self._playlist_uri_from_name:
|
||||
name = '%s [%d]' % (playlist_name, i)
|
||||
i += 1
|
||||
return name
|
||||
|
||||
def refresh_playlists_mapping(self):
|
||||
"""
|
||||
Maintain map between playlists and unique playlist names to be used by
|
||||
MPD
|
||||
"""
|
||||
if self.core is not None:
|
||||
self._playlist_uri_from_name.clear()
|
||||
self._playlist_name_from_uri.clear()
|
||||
for playlist in self.core.playlists.playlists.get():
|
||||
if not playlist.name:
|
||||
continue
|
||||
name = self.create_unique_name(playlist.name)
|
||||
self._playlist_uri_from_name[name] = playlist.uri
|
||||
self._playlist_name_from_uri[playlist.uri] = name
|
||||
|
||||
def lookup_playlist_from_name(self, name):
|
||||
"""
|
||||
Helper function to retrieve a playlist from its unique MPD name.
|
||||
"""
|
||||
if not self._playlist_uri_from_name:
|
||||
self.refresh_playlists_mapping()
|
||||
if name not in self._playlist_uri_from_name:
|
||||
return None
|
||||
uri = self._playlist_uri_from_name[name]
|
||||
return self.core.playlists.lookup(uri).get()
|
||||
|
||||
def lookup_playlist_name_from_uri(self, uri):
|
||||
"""
|
||||
Helper function to retrieve the unique MPD playlist name from its uri.
|
||||
"""
|
||||
if uri not in self._playlist_name_from_uri:
|
||||
self.refresh_playlists_mapping()
|
||||
return self._playlist_name_from_uri[uri]
|
||||
|
||||
7
mopidy/frontends/mpd/ext.conf
Normal file
7
mopidy/frontends/mpd/ext.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[mpd]
|
||||
enabled = true
|
||||
hostname = 127.0.0.1
|
||||
port = 6600
|
||||
password =
|
||||
max_connections = 20
|
||||
connection_timeout = 60
|
||||
@ -1,6 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import (
|
||||
MpdPasswordError, MpdPermissionError)
|
||||
@ -40,7 +39,7 @@ def password_(context, password):
|
||||
This is used for authentication with the server. ``PASSWORD`` is
|
||||
simply the plaintext password.
|
||||
"""
|
||||
if password == settings.MPD_SERVER_PASSWORD:
|
||||
if password == context.config['mpd']['password']:
|
||||
context.dispatcher.authenticated = True
|
||||
else:
|
||||
raise MpdPasswordError('incorrect password', command='password')
|
||||
|
||||
@ -381,10 +381,8 @@ def searchaddpl(context, playlist_name, mpd_query):
|
||||
return
|
||||
results = context.core.library.search(**query).get()
|
||||
|
||||
playlists = context.core.playlists.filter(name=playlist_name).get()
|
||||
if playlists:
|
||||
playlist = playlists[0]
|
||||
else:
|
||||
playlist = context.lookup_playlist_from_name(playlist_name)
|
||||
if not playlist:
|
||||
playlist = context.core.playlists.create(playlist_name).get()
|
||||
tracks = list(playlist.tracks) + _get_tracks(results)
|
||||
playlist = playlist.copy(tracks=tracks)
|
||||
|
||||
@ -23,10 +23,10 @@ def listplaylist(context, name):
|
||||
file: relative/path/to/file2.ogg
|
||||
file: relative/path/to/file3.mp3
|
||||
"""
|
||||
playlists = context.core.playlists.filter(name=name).get()
|
||||
if not playlists:
|
||||
playlist = context.lookup_playlist_from_name(name)
|
||||
if not playlist:
|
||||
raise MpdNoExistError('No such playlist', command='listplaylist')
|
||||
return ['file: %s' % t.uri for t in playlists[0].tracks]
|
||||
return ['file: %s' % t.uri for t in playlist.tracks]
|
||||
|
||||
|
||||
@handle_request(r'^listplaylistinfo (?P<name>\w+)$')
|
||||
@ -44,10 +44,10 @@ def listplaylistinfo(context, name):
|
||||
Standard track listing, with fields: file, Time, Title, Date,
|
||||
Album, Artist, Track
|
||||
"""
|
||||
playlists = context.core.playlists.filter(name=name).get()
|
||||
if not playlists:
|
||||
playlist = context.lookup_playlist_from_name(name)
|
||||
if not playlist:
|
||||
raise MpdNoExistError('No such playlist', command='listplaylistinfo')
|
||||
return playlist_to_mpd_format(playlists[0])
|
||||
return playlist_to_mpd_format(playlist)
|
||||
|
||||
|
||||
@handle_request(r'^listplaylists$')
|
||||
@ -80,7 +80,8 @@ def listplaylists(context):
|
||||
for playlist in context.core.playlists.playlists.get():
|
||||
if not playlist.name:
|
||||
continue
|
||||
result.append(('playlist', playlist.name))
|
||||
name = context.lookup_playlist_name_from_uri(playlist.uri)
|
||||
result.append(('playlist', name))
|
||||
last_modified = (
|
||||
playlist.last_modified or dt.datetime.utcnow()).isoformat()
|
||||
# Remove microseconds
|
||||
@ -113,14 +114,14 @@ def load(context, name, start=None, end=None):
|
||||
- MPD 0.17.1 does not fail if the specified range is outside the playlist,
|
||||
in either or both ends.
|
||||
"""
|
||||
playlists = context.core.playlists.filter(name=name).get()
|
||||
if not playlists:
|
||||
playlist = context.lookup_playlist_from_name(name)
|
||||
if not playlist:
|
||||
raise MpdNoExistError('No such playlist', command='load')
|
||||
if start is not None:
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
context.core.tracklist.add(playlists[0].tracks[start:end])
|
||||
context.core.tracklist.add(playlist.tracks[start:end])
|
||||
|
||||
|
||||
@handle_request(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
|
||||
@ -18,9 +18,10 @@ class MpdSession(network.LineProtocol):
|
||||
encoding = protocol.ENCODING
|
||||
delimiter = r'\r?\n'
|
||||
|
||||
def __init__(self, connection, core=None):
|
||||
def __init__(self, connection, config=None, core=None):
|
||||
super(MpdSession, self).__init__(connection)
|
||||
self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core)
|
||||
self.dispatcher = dispatcher.MpdDispatcher(
|
||||
session=self, config=config, core=core)
|
||||
|
||||
def on_start(self):
|
||||
logger.info('New MPD connection from [%s]:%s', self.host, self.port)
|
||||
|
||||
@ -5,7 +5,6 @@ import re
|
||||
import shlex
|
||||
import urllib
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd import protocol
|
||||
from mopidy.frontends.mpd.exceptions import MpdArgError
|
||||
from mopidy.models import TlTrack
|
||||
@ -216,12 +215,14 @@ def query_from_mpd_search_format(mpd_query):
|
||||
return query
|
||||
|
||||
|
||||
def tracks_to_tag_cache_format(tracks):
|
||||
def tracks_to_tag_cache_format(tracks, media_dir):
|
||||
"""
|
||||
Format list of tracks for output to MPD tag cache
|
||||
|
||||
:param tracks: the tracks
|
||||
:type tracks: list of :class:`mopidy.models.Track`
|
||||
:param media_dir: the path to the music dir
|
||||
:type media_dir: string
|
||||
:rtype: list of lists of two-tuples
|
||||
"""
|
||||
result = [
|
||||
@ -231,14 +232,15 @@ def tracks_to_tag_cache_format(tracks):
|
||||
('info_end',)
|
||||
]
|
||||
tracks.sort(key=lambda t: t.uri)
|
||||
_add_to_tag_cache(result, *tracks_to_directory_tree(tracks))
|
||||
dirs, files = tracks_to_directory_tree(tracks, media_dir)
|
||||
_add_to_tag_cache(result, dirs, files, media_dir)
|
||||
return result
|
||||
|
||||
# TODO: bytes only
|
||||
def _add_to_tag_cache(result, dirs, files, media_dir):
|
||||
base_path = media_dir.encode('utf-8')
|
||||
|
||||
def _add_to_tag_cache(result, folders, files):
|
||||
base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8')
|
||||
|
||||
for path, entry in folders.items():
|
||||
for path, (entry_dirs, entry_files) in dirs.items():
|
||||
try:
|
||||
text_path = path.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
@ -247,7 +249,7 @@ def _add_to_tag_cache(result, folders, files):
|
||||
result.append(('directory', text_path))
|
||||
result.append(('mtime', get_mtime(os.path.join(base_path, path))))
|
||||
result.append(('begin', name))
|
||||
_add_to_tag_cache(result, *entry)
|
||||
_add_to_tag_cache(result, entry_dirs, entry_files, media_dir)
|
||||
result.append(('end', name))
|
||||
|
||||
result.append(('songList begin',))
|
||||
@ -273,7 +275,7 @@ def _add_to_tag_cache(result, folders, files):
|
||||
result.append(('songList end',))
|
||||
|
||||
|
||||
def tracks_to_directory_tree(tracks):
|
||||
def tracks_to_directory_tree(tracks, media_dir):
|
||||
directories = ({}, [])
|
||||
|
||||
for track in tracks:
|
||||
@ -282,8 +284,7 @@ def tracks_to_directory_tree(tracks):
|
||||
|
||||
absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri))
|
||||
relative_track_dir_path = re.sub(
|
||||
'^' + re.escape(settings.LOCAL_MUSIC_PATH), b'',
|
||||
absolute_track_dir_path)
|
||||
'^' + re.escape(media_dir), b'', absolute_track_dir_path)
|
||||
|
||||
for part in split_path(relative_track_dir_path):
|
||||
path = os.path.join(path, part)
|
||||
|
||||
@ -1,56 +1,36 @@
|
||||
"""
|
||||
Frontend which lets you control Mopidy through the Media Player Remote
|
||||
Interfacing Specification (`MPRIS <http://www.mpris.org/>`_) D-Bus
|
||||
interface.
|
||||
|
||||
An example of an MPRIS client is the `Ubuntu Sound Menu
|
||||
<https://wiki.ubuntu.com/SoundMenu>`_.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- D-Bus Python bindings. The package is named ``python-dbus`` in
|
||||
Ubuntu/Debian.
|
||||
|
||||
- ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
|
||||
Ubuntu Sound Menu. The package is named ``python-indicate`` in
|
||||
Ubuntu/Debian.
|
||||
|
||||
- An ``.desktop`` file for Mopidy installed at the path set in
|
||||
:attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install-desktop-file` for
|
||||
details.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.DESKTOP_FILE`
|
||||
|
||||
**Usage:**
|
||||
|
||||
Make sure :attr:`mopidy.settings.FRONTENDS` includes
|
||||
``mopidy.frontends.mpris.MprisFrontend``. By default, the setting includes the
|
||||
MPRIS frontend.
|
||||
|
||||
**Testing the frontend**
|
||||
|
||||
To test, start Mopidy, and then run the following in a Python shell::
|
||||
|
||||
import dbus
|
||||
bus = dbus.SessionBus()
|
||||
player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
|
||||
'/org/mpris/MediaPlayer2')
|
||||
|
||||
Now you can control Mopidy through the player object. Examples:
|
||||
|
||||
- To get some properties from Mopidy, run::
|
||||
|
||||
props = player.GetAll('org.mpris.MediaPlayer2',
|
||||
dbus_interface='org.freedesktop.DBus.Properties')
|
||||
|
||||
- To quit Mopidy through D-Bus, run::
|
||||
|
||||
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import MprisFrontend
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, exceptions, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-MPRIS'
|
||||
ext_name = 'mpris'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['desktop_file'] = config.Path()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
if 'DISPLAY' not in os.environ:
|
||||
raise exceptions.ExtensionError(
|
||||
'An X11 $DISPLAY is needed to use D-Bus')
|
||||
|
||||
try:
|
||||
import dbus # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('dbus library not found', e)
|
||||
|
||||
def get_frontend_classes(self):
|
||||
from .actor import MprisFrontend
|
||||
return [MprisFrontend]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user