Release v0.14.0
This commit is contained in:
commit
027b0e2e8c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
.coverage
|
.coverage
|
||||||
|
|||||||
1
.mailmap
1
.mailmap
@ -1,5 +1,6 @@
|
|||||||
Thomas Adamcik <thomas@adamcik.no> <adamcik@samfundet.no>
|
Thomas Adamcik <thomas@adamcik.no> <adamcik@samfundet.no>
|
||||||
Thomas Adamcik <thomas@adamcik.no> <thomas+github@adamcik.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>
|
Kristian Klette <klette@samfundet.no>
|
||||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
|
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
|
||||||
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
|
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
|
||||||
|
|||||||
1
AUTHORS
1
AUTHORS
@ -18,3 +18,4 @@
|
|||||||
- herrernst <herr.ernst@gmail.com>
|
- herrernst <herr.ernst@gmail.com>
|
||||||
- Nick Steel <kingosticks@gmail.com>
|
- Nick Steel <kingosticks@gmail.com>
|
||||||
- Zan Dobersek <zandobersek@gmail.com>
|
- Zan Dobersek <zandobersek@gmail.com>
|
||||||
|
- Thomas Refis <refis.thomas@gmail.com>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
include *.ini
|
|
||||||
include *.rst
|
include *.rst
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include MANIFEST.in
|
include MANIFEST.in
|
||||||
include data/mopidy.desktop
|
include data/mopidy.desktop
|
||||||
include mopidy/backends/spotify/spotify_appkey.key
|
include mopidy/backends/spotify/spotify_appkey.key
|
||||||
include pylintrc
|
include pylintrc
|
||||||
|
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
prune docs/_build
|
prune docs/_build
|
||||||
recursive-include mopidy/frontends/http/data/
|
|
||||||
|
recursive-include mopidy *.conf
|
||||||
|
recursive-include mopidy/frontends/http/data *
|
||||||
recursive-include requirements *
|
recursive-include requirements *
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
recursive-include tests/data *
|
recursive-include tests/data *
|
||||||
|
|||||||
32
README.rst
32
README.rst
@ -2,25 +2,27 @@
|
|||||||
Mopidy
|
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
|
To control your Mopidy music server, you can use one of Mopidy's web clients,
|
||||||
and from Spotify. Searches returns results from both your local hard drive and
|
the Ubuntu Sound Menu, any device on the same network which can control UPnP
|
||||||
from Spotify, and you can mix tracks from both sources in your play queue. Your
|
MediaRenderers, or any MPD client. MPD clients are available for many
|
||||||
Spotify playlists are also available for use, though we don't support modifying
|
platforms, including Windows, OS X, Linux, Android and iOS.
|
||||||
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 get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||||
|
|
||||||
- `Documentation <http://docs.mopidy.com/>`_
|
- `Documentation <http://docs.mopidy.com/>`_
|
||||||
- `Source code <http://github.com/mopidy/mopidy>`_
|
- `Source code <https://github.com/mopidy/mopidy>`_
|
||||||
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
|
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||||
- `CI server <http://travis-ci.org/mopidy/mopidy>`_
|
- `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/>`_
|
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
- 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`.
|
the :ref:`core-api`.
|
||||||
|
|
||||||
|
|
||||||
|
Backend class
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. autoclass:: mopidy.backends.base.Backend
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Playback provider
|
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
|
<http://pykka.readthedocs.org/>`_ actor, called the "main actor" from here
|
||||||
on.
|
on.
|
||||||
|
|
||||||
- The main actor MUST accept a constructor argument ``core``, which will be an
|
- The main actor MUST accept two constructor arguments:
|
||||||
:class:`ActorProxy <pykka.proxy.ActorProxy>` for the core actor. This object
|
|
||||||
gives access to the full :ref:`core-api`.
|
- ``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
|
- It MAY use additional actors to implement whatever it does, and using actors
|
||||||
in frontend implementations is encouraged.
|
in frontend implementations is encouraged.
|
||||||
|
|
||||||
- The frontend is activated by including its main actor in the
|
- The frontend is enabled if the extension it is part of is enabled. See
|
||||||
:attr:`mopidy.settings.FRONTENDS` setting.
|
:ref:`extensiondev` for more information.
|
||||||
|
|
||||||
- The main actor MUST be able to start and stop the frontend when the main
|
- The main actor MUST be able to start and stop the frontend when the main
|
||||||
actor is started and stopped.
|
actor is started and stopped.
|
||||||
@ -45,6 +48,6 @@ Frontend implementations
|
|||||||
========================
|
========================
|
||||||
|
|
||||||
* :mod:`mopidy.frontends.http`
|
* :mod:`mopidy.frontends.http`
|
||||||
* :mod:`mopidy.frontends.lastfm`
|
|
||||||
* :mod:`mopidy.frontends.mpd`
|
* :mod:`mopidy.frontends.mpd`
|
||||||
* :mod:`mopidy.frontends.mpris`
|
* :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
|
API reference
|
||||||
*************
|
*************
|
||||||
@ -11,3 +13,6 @@ API reference
|
|||||||
core
|
core
|
||||||
audio
|
audio
|
||||||
frontends
|
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
|
and immutable. In other words, they can only be set through the class
|
||||||
constructor during instance creation.
|
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
|
Data model relations
|
||||||
====================
|
====================
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
.. _authors:
|
||||||
|
|
||||||
*******
|
*******
|
||||||
Authors
|
Authors
|
||||||
*******
|
*******
|
||||||
@ -6,11 +8,7 @@ Contributors to Mopidy in the order of appearance:
|
|||||||
|
|
||||||
.. include:: ../AUTHORS
|
.. include:: ../AUTHORS
|
||||||
|
|
||||||
|
|
||||||
Showing your appreciation
|
|
||||||
=========================
|
|
||||||
|
|
||||||
If you already enjoy Mopidy, or don't enjoy it and want to help us making
|
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.
|
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
|
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)
|
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**
|
**HTTP frontend**
|
||||||
|
|
||||||
- Added new optional HTTP frontend which exposes Mopidy's core API through
|
- 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.
|
details.
|
||||||
|
|
||||||
- Added a JavaScript library, Mopidy.js, to make it easier to develop web based
|
- 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
|
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
|
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
|
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
|
updated release roadmap with our goals for the 0.1 to 0.3 releases.
|
||||||
releases.
|
|
||||||
|
|
||||||
Enjoy the best alpha relase of Mopidy ever :-)
|
Enjoy the best alpha relase of Mopidy ever :-)
|
||||||
|
|
||||||
@ -4,14 +4,15 @@
|
|||||||
HTTP clients
|
HTTP clients
|
||||||
************
|
************
|
||||||
|
|
||||||
Mopidy added an :ref:`HTTP frontend <http-frontend>` in 0.10 which provides the
|
Mopidy added an :ref:`HTTP frontend <ext-http>` and an :ref:`HTTP API
|
||||||
building blocks needed for creating web clients for Mopidy with the help of a
|
<http-api>` in 0.10 which together provides the building blocks needed for
|
||||||
WebSocket and a JavaScript library provided by Mopidy.
|
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
|
This page will list any Mopidy web clients using the HTTP frontend. If you've
|
||||||
notify us so we can include your client on this page.
|
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
|
woutervanwijk/Mopidy-Webclient
|
||||||
@ -24,9 +25,9 @@ woutervanwijk/Mopidy-Webclient
|
|||||||
The first web client for Mopidy is still under development, but is already very
|
The first web client for Mopidy is still under development, but is already very
|
||||||
usable. It targets both desktop and mobile browsers.
|
usable. It targets both desktop and mobile browsers.
|
||||||
|
|
||||||
To try it out, get a copy of https://github.com/woutervanwijk/Mopidy-WebClient
|
The web client used for the `Pi Musicbox
|
||||||
and point the :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` setting towards
|
<http://www.woutervanwijk.nl/pimusicbox/>`_ is also available for other users
|
||||||
your copy of the web client.
|
of Mopidy. See https://github.com/woutervanwijk/Mopidy-WebClient for details.
|
||||||
|
|
||||||
|
|
||||||
Mopidy Lux
|
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.
|
https://github.com/meantimeit/jukepi for details.
|
||||||
|
|
||||||
|
|
||||||
Rompr
|
Other web clients
|
||||||
=====
|
=================
|
||||||
|
|
||||||
.. image:: /_static/rompr.png
|
For Mopidy web clients using Mopidy's MPD frontend instead of HTTP, see
|
||||||
:width: 557
|
:ref:`mpd-web-clients`.
|
||||||
: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."
|
|
||||||
|
|||||||
@ -320,7 +320,29 @@ purchased from `MPaD at iTunes Store
|
|||||||
when waiting for the connection to a server to succeed.
|
when waiting for the connection to a server to succeed.
|
||||||
|
|
||||||
|
|
||||||
|
.. _mpd-web-clients:
|
||||||
|
|
||||||
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
|
Specification. It's a spec that describes a standard D-Bus interface for making
|
||||||
media players available to other applications on the same system.
|
media players available to other applications on the same system.
|
||||||
|
|
||||||
Mopidy's :ref:`MPRIS frontend <mpris-frontend>` currently implements all
|
Mopidy's :ref:`MPRIS frontend <ext-mpris>` currently implements all required
|
||||||
required parts of the MPRIS spec, plus the optional playlist interface. It does
|
parts of the MPRIS spec, plus the optional playlist interface. It does not
|
||||||
not implement the optional tracklist interface.
|
implement the optional tracklist interface.
|
||||||
|
|
||||||
|
|
||||||
.. _ubuntu-sound-menu:
|
.. _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.
|
Mopidy is running.
|
||||||
|
|
||||||
Next, Mopidy's MPRIS frontend must be running for the sound menu to be able to
|
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
|
control Mopidy. The frontend is enabled by default, so as long as you have all
|
||||||
the :attr:`mopidy.settings.FRONTENDS` setting, you should be good to go. Keep
|
its dependencies available, you should be good to go. Keep an eye out for
|
||||||
an eye out for warnings or errors from the MPRIS frontend when you start
|
warnings or errors from the MPRIS frontend when you start Mopidy, since it may
|
||||||
Mopidy, since it may fail because of missing dependencies or because Mopidy is
|
fail because of missing dependencies or because Mopidy is started outside of X;
|
||||||
started outside of X; the frontend won't work if ``$DISPLAY`` isn't set when
|
the frontend won't work if ``$DISPLAY`` isn't set when Mopidy is started.
|
||||||
Mopidy is started.
|
|
||||||
|
|
||||||
Under normal use, if Mopidy isn't running and you open the menu and click on
|
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
|
"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
|
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
|
be made available as an UPnP MediaRenderer. Rygel will interface with Mopidy's
|
||||||
:ref:`MPRIS frontend <mpris-frontend>`, and make Mopidy available as a
|
:ref:`MPRIS frontend <ext-mpris>`, and make Mopidy available as a MediaRenderer
|
||||||
MediaRenderer on the local network. Since this depends on the MPRIS frontend,
|
on the local network. Since this depends on the MPRIS frontend, which again
|
||||||
which again depends on D-Bus being available, this will only work on Linux, and
|
depends on D-Bus being available, this will only work on Linux, and not OS X.
|
||||||
not OS X. MPRIS/D-Bus is only available to other applications on the same host,
|
MPRIS/D-Bus is only available to other applications on the same host, so Rygel
|
||||||
so Rygel must be running on the same machine as Mopidy.
|
must be running on the same machine as Mopidy.
|
||||||
|
|
||||||
1. Start Mopidy and make sure the :ref:`MPRIS frontend <mpris-frontend>` is
|
1. Start Mopidy and make sure the :ref:`MPRIS frontend <ext-mpris>` is working.
|
||||||
working. It is activated by default, but you may miss dependencies or be
|
It is activated by default, but you may miss dependencies or be using OS X,
|
||||||
using OS X, in which case it will not work. Check the console output when
|
in which case it will not work. Check the console output when Mopidy is
|
||||||
Mopidy is started for any errors related to the MPRIS frontend. If you're
|
started for any errors related to the MPRIS frontend. If you're unsure it is
|
||||||
unsure it is working, there are instructions for how to test it on the
|
working, there are instructions for how to test it on the :ref:`MPRIS
|
||||||
:ref:`MPRIS frontend <mpris-frontend>` page.
|
frontend <ext-mpris>` page.
|
||||||
|
|
||||||
2. Install Rygel. On Debian/Ubuntu::
|
2. Install Rygel. On Debian/Ubuntu::
|
||||||
|
|
||||||
@ -66,11 +66,10 @@ so Rygel must be running on the same machine as Mopidy.
|
|||||||
|
|
||||||
$ rygel
|
$ rygel
|
||||||
Rygel-Message: New plugin 'MediaExport' available
|
Rygel-Message: New plugin 'MediaExport' available
|
||||||
Rygel-Message: New plugin 'org.mpris.MediaPlayer2.spotify' available
|
|
||||||
Rygel-Message: New plugin 'org.mpris.MediaPlayer2.mopidy' available
|
Rygel-Message: New plugin 'org.mpris.MediaPlayer2.mopidy' available
|
||||||
|
|
||||||
Note that in the above example, both the official Spotify client and Mopidy
|
In the above example, you can see that Rygel found Mopidy, and it is now
|
||||||
is running and made available through Rygel.
|
making Mopidy available through Rygel.
|
||||||
|
|
||||||
|
|
||||||
The UPnP-Inspector client
|
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):
|
def __getattr__(self, name):
|
||||||
if name in ('__file__', '__path__'):
|
if name in ('__file__', '__path__'):
|
||||||
return '/dev/null'
|
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, (), {})
|
return type(name, (), {})
|
||||||
else:
|
else:
|
||||||
return Mock()
|
return Mock()
|
||||||
@ -53,7 +54,6 @@ MOCK_MODULES = [
|
|||||||
'pykka.future',
|
'pykka.future',
|
||||||
'pykka.registry',
|
'pykka.registry',
|
||||||
'pylast',
|
'pylast',
|
||||||
'serial',
|
|
||||||
'ws4py',
|
'ws4py',
|
||||||
'ws4py.messaging',
|
'ws4py.messaging',
|
||||||
'ws4py.server',
|
'ws4py.server',
|
||||||
@ -98,7 +98,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Mopidy'
|
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
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
@ -266,3 +266,12 @@ latex_documents = [
|
|||||||
needs_sphinx = '1.0'
|
needs_sphinx = '1.0'
|
||||||
|
|
||||||
extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', '#')}
|
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
|
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.
|
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
|
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
|
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
|
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
|
based Mopidy clients possible. In Mopidy 0.9 we added support for multiple
|
||||||
@ -30,22 +25,21 @@ extension to behave.
|
|||||||
Anatomy of an extension
|
Anatomy of an extension
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
Extensions are all located in a Python package called ``mopidy_something``
|
Extensions are located in a Python package called ``mopidy_something`` where
|
||||||
where "something" is the name of the application, library or web service you
|
"something" is the name of the application, library or web service you want to
|
||||||
want to integrated with Mopidy. So for example if you plan to add support for a
|
integrated with Mopidy. So for example if you plan to add support for a service
|
||||||
service named Soundspot to Mopidy, you would name your extension's Python
|
named Soundspot to Mopidy, you would name your extension's Python package
|
||||||
package ``mopidy_soundspot``.
|
``mopidy_soundspot``.
|
||||||
|
|
||||||
The name of the actual extension (the human readable name) however would be
|
The extension must be shipped with a ``setup.py`` file and be registered on
|
||||||
something like "Mopidy-Soundspot". Make sure to include the name "Mopidy"
|
`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
|
somewhere in that name and that you check the capitalization. This is the name
|
||||||
users will use when they install your extension from PyPI.
|
users will use when they install your extension from PyPI.
|
||||||
|
|
||||||
The extension must be shipped with a ``setup.py`` file and be registered on
|
Also make sure the development version link in your package details work so
|
||||||
`PyPI <https://pypi.python.org/>`_. Also make sure the development version link
|
that people can easily install the development version into their virtualenv
|
||||||
in your package details work so that people can easily install the development
|
simply by running e.g. ``pip install Mopidy-Soundspot==dev``.
|
||||||
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),
|
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
|
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
|
mopidy-soundspot/ # The Git repo root
|
||||||
LICENSE # The license text
|
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
|
README.rst # Document what it is and how to use it
|
||||||
mopidy_soundspot/ # Your code
|
mopidy_soundspot/ # Your code
|
||||||
__init__.py
|
__init__.py
|
||||||
config.ini # Default configuration for the extension
|
ext.conf # Default config for the extension
|
||||||
...
|
...
|
||||||
setup.py # Installation script
|
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,
|
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
|
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
|
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
|
that the development snapshot link ends with ``#egg=Mopidy-Something-dev`` for
|
||||||
installation using ``pip install mopidy-something==dev`` to work.
|
installation using ``pip install Mopidy-Something==dev`` to work.
|
||||||
|
|
||||||
.. code-block:: rst
|
.. code-block:: rst
|
||||||
|
|
||||||
@ -108,7 +103,7 @@ installation using ``pip install mopidy-something==dev`` to work.
|
|||||||
|
|
||||||
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
|
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
|
||||||
- `Issue tracker <https://github.com/mopidy/mopidy-soundspot/issues>`_
|
- `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
|
Example setup.py
|
||||||
@ -120,18 +115,18 @@ register themselves as available Mopidy extensions when they are installed on
|
|||||||
your system.
|
your system.
|
||||||
|
|
||||||
The example below also includes a couple of convenient tricks for reading the
|
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
|
package version from the source code so that it is defined in a single place,
|
||||||
place, and to reuse the README file as the long description of the package for
|
and to reuse the README file as the long description of the package for the
|
||||||
the PyPI registration.
|
PyPI registration.
|
||||||
|
|
||||||
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy``, in
|
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy``, in
|
||||||
addition to any other dependencies required by your extension. The
|
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
|
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 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
|
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.
|
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',
|
description='Very short description',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.rst').read(),
|
||||||
packages=['mopidy_soundspot'],
|
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,
|
zip_safe=False,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
platforms='any',
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'Mopidy',
|
'Mopidy',
|
||||||
'pysoundspot',
|
'pysoundspot',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'mopidy.extension': [
|
'mopidy.ext': [
|
||||||
'soundspot = mopidy_soundspot:Extension',
|
'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
|
Example __init__.py
|
||||||
===================
|
===================
|
||||||
|
|
||||||
The ``__init__.py`` file should be placed inside the ``mopidy_soundspot``
|
The ``__init__.py`` file should be placed inside the ``mopidy_soundspot``
|
||||||
Python package. The root of your Python package should have an ``__version__``
|
Python package.
|
||||||
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.
|
|
||||||
|
|
||||||
::
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
@ -208,8 +228,7 @@ raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
|||||||
import gst
|
import gst
|
||||||
import gobject
|
import gobject
|
||||||
|
|
||||||
from mopidy.exceptions import ExtensionError
|
from mopidy import config, exceptions, ext
|
||||||
from mopidy.utils import ext
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = '0.1'
|
__version__ = '0.1'
|
||||||
@ -217,73 +236,44 @@ raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
|||||||
|
|
||||||
class Extension(ext.Extension):
|
class Extension(ext.Extension):
|
||||||
|
|
||||||
name = 'Mopidy-Soundspot'
|
dist_name = 'Mopidy-Soundspot'
|
||||||
|
ext_name = 'soundspot'
|
||||||
version = __version__
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
def get_default_config(self):
|
||||||
def get_default_config(cls):
|
conf_file = os.path.join(os.path.dirname(__file__, 'ext.conf'))
|
||||||
config_file = os.path.join(
|
return config.read(conf_file)
|
||||||
os.path.dirname(__file__), 'config.ini')
|
|
||||||
return open(config_file).read()
|
|
||||||
|
|
||||||
@classmethod
|
def get_config_schema(self):
|
||||||
def validate_config(cls, config):
|
schema = super(Extension, self).get_config_schema()
|
||||||
# ``config`` is the complete config document for the Mopidy
|
schema['username'] = config.String()
|
||||||
# instance. The extension is free to check any config value it is
|
schema['password'] = config.Secret()
|
||||||
# interested in, not just its own config values.
|
return schema
|
||||||
|
|
||||||
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 validate_environment(self):
|
||||||
try:
|
try:
|
||||||
import pysoundspot
|
import pysoundspot
|
||||||
except ImportError as e:
|
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
|
# You will typically only implement one of the next three methods
|
||||||
# in a single extension.
|
# in a single extension.
|
||||||
|
|
||||||
@classmethod
|
def get_frontend_classes(self):
|
||||||
def get_frontend_class(cls):
|
|
||||||
from .frontend import SoundspotFrontend
|
from .frontend import SoundspotFrontend
|
||||||
return SoundspotFrontend
|
return [SoundspotFrontend]
|
||||||
|
|
||||||
@classmethod
|
def get_backend_classes(self):
|
||||||
def get_backend_class(cls):
|
|
||||||
from .backend import SoundspotBackend
|
from .backend import SoundspotBackend
|
||||||
return SoundspotBackend
|
return [SoundspotBackend]
|
||||||
|
|
||||||
@classmethod
|
def register_gstreamer_elements(self):
|
||||||
def register_gstreamer_elements(cls):
|
|
||||||
from .mixer import SoundspotMixer
|
from .mixer import SoundspotMixer
|
||||||
|
|
||||||
gobject.type_register(SoundspotMixer)
|
gobject.type_register(SoundspotMixer)
|
||||||
gst.element_register(
|
gst.element_register(
|
||||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||||
|
|
||||||
|
And this is ``mopidy_soundspot/ext.conf``:
|
||||||
|
|
||||||
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``.
|
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
@ -292,6 +282,8 @@ like ``username`` and ``password``.
|
|||||||
username =
|
username =
|
||||||
password =
|
password =
|
||||||
|
|
||||||
|
For more detailed documentation on the extension class, see the :ref:`ext-api`.
|
||||||
|
|
||||||
|
|
||||||
Example frontend
|
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.
|
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
|
Basically, you just implement your GStreamer element in Python and then make
|
||||||
your :meth:`Extension.register_gstreamer_elements` method register all your
|
your :meth:`~mopidy.ext.Extension.register_gstreamer_elements` method register
|
||||||
custom GStreamer elements.
|
all your custom GStreamer elements.
|
||||||
|
|
||||||
For examples of custom GStreamer elements implemented in Python, see
|
For examples of custom GStreamer elements implemented in Python, see
|
||||||
:mod:`mopidy.audio.mixers`.
|
:mod:`mopidy.audio.mixers`.
|
||||||
|
|
||||||
|
|
||||||
Implementation steps
|
Python conventions
|
||||||
====================
|
==================
|
||||||
|
|
||||||
A rough plan of how to make the above document the reality of how Mopidy
|
In general, it would be nice if Mopidy extensions followed the same
|
||||||
extensions work.
|
: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
|
Use of Mopidy APIs
|
||||||
that we can register entry points for the bundled extensions and get
|
==================
|
||||||
information about all extensions available on the system from
|
|
||||||
:mod:`pkg_resources`.
|
|
||||||
|
|
||||||
3. Add :class:`Extension` classes for all existing frontends and backends. Make
|
When writing an extension, you should only use APIs documented at
|
||||||
sure to add default config files and config validation, even though this
|
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.utils`, may change at
|
||||||
will not be used at this implementation stage.
|
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
|
Logging in extensions
|
||||||
backends via :mod:`pkg_resouces` instead of the ``FRONTENDS`` and
|
=====================
|
||||||
``BACKENDS`` settings.
|
|
||||||
|
|
||||||
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
|
import logging
|
||||||
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:
|
|
||||||
|
|
||||||
- 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,
|
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
|
||||||
- setting a config value for this execution of Mopidy,
|
:confval:`loglevels/*` config section.
|
||||||
|
|
||||||
- printing the effective config and exit, and
|
|
||||||
|
|
||||||
- write a config value permanently to ``~/.config/mopidy.conf`` and exit.
|
|
||||||
|
|||||||
@ -2,56 +2,71 @@
|
|||||||
Mopidy
|
Mopidy
|
||||||
******
|
******
|
||||||
|
|
||||||
Mopidy is a music server which can play music both from your :ref:`local hard
|
Mopidy is a music server which can play music both from multiple sources, like
|
||||||
drive <local-backend>` and from :ref:`Spotify <spotify-backend>`. Searches
|
your :ref:`local hard drive <ext-local>`, :ref:`radio streams <ext-stream>`,
|
||||||
returns results from both your local hard drive and from Spotify, and you can
|
and from :ref:`Spotify <ext-spotify>` and SoundCloud. Searches combines results
|
||||||
mix tracks from both sources in your play queue. Your Spotify playlists are
|
from all music sources, and you can mix tracks from all sources in your play
|
||||||
also available for use, though we don't support modifying them yet.
|
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
|
To control your Mopidy music server, you can use one of Mopidy's :ref:`web
|
||||||
<ubuntu-sound-menu>` on the machine running Mopidy, any device on the same
|
clients <http-clients>`, the :ref:`Ubuntu Sound Menu <ubuntu-sound-menu>`, any
|
||||||
network which can control UPnP MediaRenderers (see :ref:`upnp-clients`), or any
|
device on the same network which can control :ref:`UPnP MediaRenderers
|
||||||
:ref:`MPD client <mpd-clients>`. MPD clients are available for most platforms,
|
<upnp-clients>`, or any :ref:`MPD client <mpd-clients>`. MPD clients are
|
||||||
including Windows, Mac OS X, Linux, Android, and iOS.
|
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
|
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
|
<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
|
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
|
Usage
|
||||||
=================
|
=====
|
||||||
|
|
||||||
- `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
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 3
|
||||||
|
|
||||||
installation/index
|
installation/index
|
||||||
installation/raspberrypi
|
installation/raspberrypi
|
||||||
settings
|
config
|
||||||
|
ext/index
|
||||||
running
|
running
|
||||||
clients/index
|
clients/index
|
||||||
|
troubleshooting
|
||||||
|
|
||||||
|
|
||||||
|
About
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
authors
|
authors
|
||||||
licenses
|
licenses
|
||||||
changes
|
changelog
|
||||||
|
versioning
|
||||||
|
|
||||||
|
|
||||||
Reference documentation
|
Development
|
||||||
=======================
|
===========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
contributing
|
||||||
|
devtools
|
||||||
|
codestyle
|
||||||
|
extensiondev
|
||||||
|
|
||||||
|
|
||||||
|
Reference
|
||||||
|
=========
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
@ -60,16 +75,6 @@ Reference documentation
|
|||||||
modules/index
|
modules/index
|
||||||
|
|
||||||
|
|
||||||
Development documentation
|
|
||||||
=========================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
development
|
|
||||||
extensiondev
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
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 update
|
||||||
sudo apt-get install mopidy
|
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>`.
|
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
|
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
|
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/>`_
|
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
|
#. To install Mopidy with GStreamer, libspotify and pyspotify, you can use
|
||||||
``packer``, ``yaourt``, or do it by hand like this::
|
``packer``, ``yaourt``, or do it by hand like this::
|
||||||
@ -89,8 +90,8 @@ package found in AUR.
|
|||||||
install `python2-pylast
|
install `python2-pylast
|
||||||
<https://aur.archlinux.org/packages/python2-pylast/>`_ from AUR.
|
<https://aur.archlinux.org/packages/python2-pylast/>`_ from AUR.
|
||||||
|
|
||||||
#. 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
|
||||||
you're ready to :doc:`run Mopidy </running>`.
|
then you're ready to :doc:`run Mopidy </running>`.
|
||||||
|
|
||||||
|
|
||||||
OS X: Install from Homebrew and Pip
|
OS X: Install from Homebrew and Pip
|
||||||
@ -107,13 +108,19 @@ Pip.
|
|||||||
brew update
|
brew update
|
||||||
brew upgrade
|
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::
|
#. 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
|
#. Make sure to include Homebrew's Python ``site-packages`` directory in your
|
||||||
``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
|
``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
|
You can either amend your ``PYTHONPATH`` permanently, by adding the
|
||||||
following statement to your shell's init file, e.g. ``~/.bashrc``::
|
following statement to your shell's init file, e.g. ``~/.bashrc``::
|
||||||
@ -135,13 +142,13 @@ Pip.
|
|||||||
|
|
||||||
sudo easy_install 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::
|
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
|
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||||
you're ready to :doc:`run Mopidy </running>`.
|
then you're ready to :doc:`run Mopidy </running>`.
|
||||||
|
|
||||||
|
|
||||||
Otherwise: Install from source using Pip
|
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
|
sudo yum install -y gcc python-devel python-pip
|
||||||
|
|
||||||
#. Then you'll need to install all of Mopidy's hard dependencies:
|
#. Then you'll need to install all of Mopidy's hard non-Python dependencies:
|
||||||
|
|
||||||
- Pykka >= 1.0::
|
|
||||||
|
|
||||||
sudo pip install -U pykka
|
|
||||||
|
|
||||||
On Fedora the binary is called ``pip-python``::
|
|
||||||
|
|
||||||
sudo pip-python install -U pykka
|
|
||||||
|
|
||||||
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
|
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
|
||||||
popular Linux distributions. Search for GStreamer in your package manager,
|
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
|
sudo pip install -U pyspotify
|
||||||
|
|
||||||
# Fedora:
|
On Fedora the binary is called ``pip-python``::
|
||||||
|
|
||||||
sudo pip-python install -U pyspotify
|
sudo pip-python install -U pyspotify
|
||||||
|
|
||||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
#. 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
|
sudo pip install -U pylast
|
||||||
|
|
||||||
# Fedora:
|
On Fedora the binary is called ``pip-python``::
|
||||||
|
|
||||||
sudo pip-python install -U pylast
|
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
|
#. 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
|
Menu or from an UPnP client via Rygel, you need some additional
|
||||||
dependencies: the Python bindings for libindicate, and the Python bindings
|
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
|
sudo pip install -U mopidy
|
||||||
|
|
||||||
# Fedora:
|
On Fedora the binary is called ``pip-python``::
|
||||||
|
|
||||||
sudo pip-python install -U mopidy
|
sudo pip-python install -U mopidy
|
||||||
|
|
||||||
To upgrade Mopidy to future releases, just rerun this command.
|
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
|
sudo pip install mopidy==dev
|
||||||
|
|
||||||
#. 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
|
||||||
you're ready to :doc:`run Mopidy </running>`.
|
then you're ready to :doc:`run Mopidy </running>`.
|
||||||
|
|||||||
@ -4,13 +4,8 @@
|
|||||||
Installation on Raspberry Pi
|
Installation on Raspberry Pi
|
||||||
****************************
|
****************************
|
||||||
|
|
||||||
As of early August, 2012, running Mopidy on a `Raspberry Pi
|
Mopidy runs nicely on a `Raspberry Pi <http://www.raspberrypi.org/>`_. As of
|
||||||
<http://www.raspberrypi.org/>`_ is possible, although there are a few
|
January 2013, Mopidy will run with Spotify support on both the armel
|
||||||
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
|
|
||||||
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
|
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
|
||||||
distribution.
|
distribution.
|
||||||
|
|
||||||
@ -19,167 +14,28 @@ distribution.
|
|||||||
:height: 427
|
: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:
|
.. _raspi-wheezy:
|
||||||
|
|
||||||
How to for Debian 7 (Wheezy)
|
How to for Debian 7 (Wheezy)
|
||||||
============================
|
============================
|
||||||
|
|
||||||
This is a very similar system to Debian 6.0 above, but with a bit newer
|
#. Download the latest wheezy disk image from
|
||||||
software packages, as Wheezy is going to be the next release of Debian.
|
|
||||||
|
|
||||||
1. Download the latest wheezy disk image from
|
|
||||||
http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
|
http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
|
||||||
2012-08-08.
|
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.
|
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
|
systems on another computer. Just boot up your Raspberry Pi with the
|
||||||
unaltered partions, and it will boot right into the ``raspi-config`` tool,
|
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
|
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.
|
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
|
#. You can login to the default user using username ``pi`` and password
|
||||||
default user using username ``pi`` and password ``raspberry``. To become
|
``raspberry``. To become root, just enter ``sudo -i``.
|
||||||
root, just enter ``sudo -i``.
|
|
||||||
|
|
||||||
Opposed to on Squeeze, there is no need to add your user to the ``audio``
|
#. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
|
||||||
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:
|
|
||||||
|
|
||||||
- Load the IPv6 kernel module now::
|
- 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
|
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::
|
<http://apt.mopidy.com/>`_, as described in :ref:`installation`. In short::
|
||||||
|
|
||||||
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
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 update
|
||||||
sudo apt-get install mopidy
|
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::
|
connector, I have to run::
|
||||||
|
|
||||||
amixer cset numid=3 1
|
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
|
aplay /usr/share/sounds/alsa/Front_Center.wav
|
||||||
|
|
||||||
If you hear a voice saying "Front Center", then your sound is working. Don't
|
If you hear a voice saying "Front Center", then your sound is working.
|
||||||
be concerned if this test sound includes static. Test your sound with
|
|
||||||
GStreamer to determine the sound quality of Mopidy.
|
|
||||||
|
|
||||||
To make the change to analog output stick, you can add the ``amixer``
|
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
|
command to e.g. ``/etc/rc.local``, which will be executed when the system is
|
||||||
booting.
|
booting.
|
||||||
|
|
||||||
|
|
||||||
Audio quality issues
|
Fixing 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.
|
|
||||||
|
|
||||||
As of January 2013, some reports also indicate that pushing the audio through
|
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
|
PulseAudio may help. We hope to, in the future, provide a complete set of
|
||||||
instructions here leading to acceptable analog audio quality.
|
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
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -26,7 +26,7 @@ limitations under the License.
|
|||||||
Documentation 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
|
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||||
Unported License. To view a copy of this license, visit
|
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
|
:mod:`mopidy.frontends.mpd` -- MPD server
|
||||||
*****************************************
|
*****************************************
|
||||||
|
|
||||||
|
For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
|
||||||
|
|
||||||
.. automodule:: mopidy.frontends.mpd
|
.. automodule:: mopidy.frontends.mpd
|
||||||
:synopsis: MPD server frontend
|
: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`
|
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.
|
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
|
Using the library
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
See Mopidy's [HTTP frontend
|
See Mopidy's [HTTP API
|
||||||
documentation](http://docs.mopidy.com/en/latest/modules/frontends/http/).
|
documentation](http://docs.mopidy.com/en/latest/api/http/).
|
||||||
|
|
||||||
|
|
||||||
Building from source
|
Building from source
|
||||||
|
|||||||
@ -15,17 +15,12 @@ if not (2, 6) <= sys.version_info < (3,):
|
|||||||
'.'.join(map(str, sys.version_info[:3])))
|
'.'.join(map(str, sys.version_info[:3])))
|
||||||
|
|
||||||
if (isinstance(pykka.__version__, basestring)
|
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(
|
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')
|
warnings.filterwarnings('ignore', 'could not open display')
|
||||||
|
|
||||||
|
|
||||||
__version__ = '0.13.0'
|
__version__ = '0.14.0'
|
||||||
|
|
||||||
|
|
||||||
from mopidy import settings as default_settings_module
|
|
||||||
from mopidy.utils.settings import SettingsProxy
|
|
||||||
settings = SettingsProxy(default_settings_module)
|
|
||||||
|
|||||||
@ -12,18 +12,10 @@ gobject.threads_init()
|
|||||||
import pykka.debug
|
import pykka.debug
|
||||||
|
|
||||||
|
|
||||||
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
|
# Extract any command line arguments. This needs to be done before GStreamer is
|
||||||
# processing by GStreamer. This needs to be done before GStreamer is imported,
|
# imported, so that GStreamer doesn't hijack e.g. ``--help``.
|
||||||
# so that GStreamer doesn't hijack e.g. ``--help``.
|
mopidy_args = sys.argv[1:]
|
||||||
# NOTE This naive fix does not support values like ``bar`` in
|
sys.argv[1:] = []
|
||||||
# ``--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
|
|
||||||
|
|
||||||
|
|
||||||
# Add ../ to the path so we can run Mopidy from a Git checkout without
|
# 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__), '../')))
|
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.audio import Audio
|
||||||
|
from mopidy import config as config_lib
|
||||||
from mopidy.core import Core
|
from mopidy.core import Core
|
||||||
from mopidy.utils import (
|
from mopidy.utils import deps, log, path, process, versioning
|
||||||
deps, importing, log, path, process, settings as settings_utils,
|
|
||||||
versioning)
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.main')
|
logger = logging.getLogger('mopidy.main')
|
||||||
|
|
||||||
@ -49,44 +39,109 @@ def main():
|
|||||||
|
|
||||||
loop = gobject.MainLoop()
|
loop = gobject.MainLoop()
|
||||||
options = parse_options()
|
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:
|
try:
|
||||||
log.setup_logging(options.verbosity_level, options.save_debug_log)
|
# Initial config without extensions to bootstrap logging.
|
||||||
check_old_folders()
|
logging_config, _ = config_lib.load(config_files, [], config_overrides)
|
||||||
setup_settings(options.interactive)
|
|
||||||
audio = setup_audio()
|
# TODO: setup_logging needs defaults in-case config values are None
|
||||||
backends = setup_backends(audio)
|
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)
|
core = setup_core(audio, backends)
|
||||||
setup_frontends(core)
|
setup_frontends(proxied_config, enabled_extensions, core)
|
||||||
loop.run()
|
loop.run()
|
||||||
except exceptions.SettingsError as ex:
|
|
||||||
logger.error(ex.message)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('Interrupted. Exiting...')
|
if logging_initialized:
|
||||||
|
logger.info('Interrupted. Exiting...')
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception(ex)
|
if logging_initialized:
|
||||||
|
logger.exception(ex)
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
loop.quit()
|
loop.quit()
|
||||||
stop_frontends()
|
stop_frontends(enabled_extensions)
|
||||||
stop_core()
|
stop_core()
|
||||||
stop_backends()
|
stop_backends(enabled_extensions)
|
||||||
stop_audio()
|
stop_audio()
|
||||||
process.stop_remaining_actors()
|
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():
|
def parse_options():
|
||||||
parser = optparse.OptionParser(
|
parser = optparse.OptionParser(
|
||||||
version='Mopidy %s' % versioning.get_version())
|
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
|
# NOTE First argument to add_option must be bytestrings on Python < 2.6.2
|
||||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
# 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(
|
parser.add_option(
|
||||||
b'-q', '--quiet',
|
b'-q', '--quiet',
|
||||||
action='store_const', const=0, dest='verbosity_level',
|
action='store_const', const=0, dest='verbosity_level',
|
||||||
@ -100,89 +155,132 @@ def parse_options():
|
|||||||
action='store_true', dest='save_debug_log',
|
action='store_true', dest='save_debug_log',
|
||||||
help='save debug log to "./mopidy.log"')
|
help='save debug log to "./mopidy.log"')
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
b'--list-settings',
|
b'--show-config',
|
||||||
action='callback',
|
action='callback', callback=show_config_callback,
|
||||||
callback=settings_utils.list_settings_optparse_callback,
|
help='show current config')
|
||||||
help='list current settings')
|
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
b'--list-deps',
|
b'--show-deps',
|
||||||
action='callback', callback=deps.list_deps_optparse_callback,
|
action='callback', callback=deps.show_deps_optparse_callback,
|
||||||
help='list dependencies and their versions')
|
help='show dependencies and their versions')
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
b'--debug-thread',
|
b'--config',
|
||||||
action='store_true', dest='debug_thread',
|
action='store', dest='config',
|
||||||
help='run background thread that dumps tracebacks on SIGUSR1')
|
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]
|
return parser.parse_args(args=mopidy_args)[0]
|
||||||
|
|
||||||
|
|
||||||
def check_old_folders():
|
def show_config_callback(option, opt, value, parser):
|
||||||
old_settings_folder = os.path.expanduser('~/.mopidy')
|
# 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):
|
extensions = ext.load_extensions()
|
||||||
return
|
config, errors = config_lib.load(files, extensions, overrides)
|
||||||
|
|
||||||
logger.warning(
|
# Clear out any config for disabled extensions.
|
||||||
'Old settings folder found at %s, settings.py should be moved '
|
for extension in extensions:
|
||||||
'to %s, any cache data should be deleted. See release notes for '
|
if not ext.validate_extension(extension):
|
||||||
'further instructions.', old_settings_folder, path.SETTINGS_PATH)
|
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):
|
def check_old_locations():
|
||||||
path.get_or_create_folder(path.SETTINGS_PATH)
|
dot_mopidy_dir = path.expand_path(b'~/.mopidy')
|
||||||
path.get_or_create_folder(path.DATA_PATH)
|
if os.path.isdir(dot_mopidy_dir):
|
||||||
path.get_or_create_file(path.SETTINGS_FILE)
|
logger.warning(
|
||||||
try:
|
'Old Mopidy dot dir found at %s. Please migrate your config to '
|
||||||
settings.validate(interactive)
|
'the ini-file based config format. See release notes for further '
|
||||||
except exceptions.SettingsError as ex:
|
'instructions.', dot_mopidy_dir)
|
||||||
logger.error(ex.message)
|
|
||||||
sys.exit(1)
|
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():
|
def create_file_structures():
|
||||||
return Audio.start().proxy()
|
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():
|
def stop_audio():
|
||||||
|
logger.info('Stopping Mopidy audio')
|
||||||
process.stop_actors_by_class(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 = []
|
backends = []
|
||||||
for backend_class_name in settings.BACKENDS:
|
for backend_class in backend_classes:
|
||||||
backend_class = importing.get_class(backend_class_name)
|
backend = backend_class.start(config=config, audio=audio).proxy()
|
||||||
backend = backend_class.start(audio=audio).proxy()
|
|
||||||
backends.append(backend)
|
backends.append(backend)
|
||||||
|
|
||||||
return backends
|
return backends
|
||||||
|
|
||||||
|
|
||||||
def stop_backends():
|
def stop_backends(extensions):
|
||||||
for backend_class_name in settings.BACKENDS:
|
logger.info('Stopping Mopidy backends')
|
||||||
process.stop_actors_by_class(importing.get_class(backend_class_name))
|
for extension in extensions:
|
||||||
|
for backend_class in extension.get_backend_classes():
|
||||||
|
process.stop_actors_by_class(backend_class)
|
||||||
|
|
||||||
|
|
||||||
def setup_core(audio, backends):
|
def setup_core(audio, backends):
|
||||||
|
logger.info('Starting Mopidy core')
|
||||||
return Core.start(audio=audio, backends=backends).proxy()
|
return Core.start(audio=audio, backends=backends).proxy()
|
||||||
|
|
||||||
|
|
||||||
def stop_core():
|
def stop_core():
|
||||||
|
logger.info('Stopping Mopidy core')
|
||||||
process.stop_actors_by_class(Core)
|
process.stop_actors_by_class(Core)
|
||||||
|
|
||||||
|
|
||||||
def setup_frontends(core):
|
def setup_frontends(config, extensions, core):
|
||||||
for frontend_class_name in settings.FRONTENDS:
|
frontend_classes = []
|
||||||
try:
|
for extension in extensions:
|
||||||
importing.get_class(frontend_class_name).start(core=core)
|
frontend_classes.extend(extension.get_frontend_classes())
|
||||||
except exceptions.OptionalDependencyError as ex:
|
|
||||||
logger.info('Disabled: %s (%s)', frontend_class_name, ex)
|
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():
|
def stop_frontends(extensions):
|
||||||
for frontend_class_name in settings.FRONTENDS:
|
logger.info('Stopping Mopidy frontends')
|
||||||
try:
|
for extension in extensions:
|
||||||
frontend_class = importing.get_class(frontend_class_name)
|
for frontend_class in extension.get_frontend_classes():
|
||||||
process.stop_actors_by_class(frontend_class)
|
process.stop_actors_by_class(frontend_class)
|
||||||
except exceptions.OptionalDependencyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import logging
|
|||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.utils import process
|
from mopidy.utils import process
|
||||||
|
|
||||||
from . import mixers, utils
|
from . import mixers, utils
|
||||||
@ -27,20 +26,16 @@ MB = 1 << 20
|
|||||||
class Audio(pykka.ThreadingActor):
|
class Audio(pykka.ThreadingActor):
|
||||||
"""
|
"""
|
||||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
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`
|
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
|
||||||
state = PlaybackState.STOPPED
|
state = PlaybackState.STOPPED
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, config):
|
||||||
super(Audio, self).__init__()
|
super(Audio, self).__init__()
|
||||||
|
|
||||||
|
self._config = config
|
||||||
|
|
||||||
self._playbin = None
|
self._playbin = None
|
||||||
self._signal_ids = {} # {(element, event): signal_id}
|
self._signal_ids = {} # {(element, event): signal_id}
|
||||||
|
|
||||||
@ -143,47 +138,51 @@ class Audio(pykka.ThreadingActor):
|
|||||||
self._playbin.set_state(gst.STATE_NULL)
|
self._playbin.set_state(gst.STATE_NULL)
|
||||||
|
|
||||||
def _setup_output(self):
|
def _setup_output(self):
|
||||||
|
output_desc = self._config['audio']['output']
|
||||||
try:
|
try:
|
||||||
output = gst.parse_bin_from_description(
|
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)
|
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:
|
except gobject.GError as ex:
|
||||||
logger.error(
|
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()
|
process.exit_process()
|
||||||
|
|
||||||
def _setup_mixer(self):
|
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')
|
logger.info('Not setting up audio mixer')
|
||||||
return
|
return
|
||||||
|
|
||||||
if settings.MIXER == 'software':
|
if mixer_desc == 'software':
|
||||||
self._software_mixing = True
|
self._software_mixing = True
|
||||||
logger.info('Audio mixer is using software mixing')
|
logger.info('Audio mixer is using software mixing')
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mixerbin = gst.parse_bin_from_description(
|
mixerbin = gst.parse_bin_from_description(
|
||||||
settings.MIXER, ghost_unconnected_pads=False)
|
mixer_desc, ghost_unconnected_pads=False)
|
||||||
except gobject.GError as ex:
|
except gobject.GError as ex:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Failed to create audio mixer "%s": %s', settings.MIXER, ex)
|
'Failed to create audio mixer "%s": %s', mixer_desc, ex)
|
||||||
return
|
return
|
||||||
|
|
||||||
# We assume that the bin will contain a single mixer.
|
# We assume that the bin will contain a single mixer.
|
||||||
mixer = mixerbin.get_by_interface(b'GstMixer')
|
mixer = mixerbin.get_by_interface(b'GstMixer')
|
||||||
if not mixer:
|
if not mixer:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Did not find any audio mixers in "%s"', settings.MIXER)
|
'Did not find any audio mixers in "%s"', mixer_desc)
|
||||||
return
|
return
|
||||||
|
|
||||||
if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
|
if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Setting audio mixer "%s" to READY failed', settings.MIXER)
|
'Setting audio mixer "%s" to READY failed', mixer_desc)
|
||||||
return
|
return
|
||||||
|
|
||||||
track = self._select_mixer_track(mixer, settings.MIXER_TRACK)
|
track = self._select_mixer_track(mixer, track_desc)
|
||||||
if not track:
|
if not track:
|
||||||
logger.warning('Could not find usable audio mixer track')
|
logger.warning('Could not find usable audio mixer track')
|
||||||
return
|
return
|
||||||
@ -198,8 +197,9 @@ class Audio(pykka.ThreadingActor):
|
|||||||
|
|
||||||
def _select_mixer_track(self, mixer, track_label):
|
def _select_mixer_track(self, mixer, track_label):
|
||||||
# Ignore tracks without volumes, then look for track with
|
# Ignore tracks without volumes, then look for track with
|
||||||
# label == settings.MIXER_TRACK, otherwise fallback to first usable
|
# label equal to the audio/mixer_track config value, otherwise fallback
|
||||||
# track hoping the mixer gave them to us in a sensible order.
|
# to first usable track hoping the mixer gave them to us in a sensible
|
||||||
|
# order.
|
||||||
|
|
||||||
usable_tracks = []
|
usable_tracks = []
|
||||||
for track in mixer.list_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
|
*MAY* be implemented by actor. By default, this method forwards the
|
||||||
event to the specific event methods.
|
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
|
:param event: the event name
|
||||||
:type event: string
|
:type event: string
|
||||||
:param kwargs: any other arguments to the specific event handlers
|
:param kwargs: any other arguments to the specific event handlers
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import gobject
|
|||||||
|
|
||||||
from .auto import AutoAudioMixer
|
from .auto import AutoAudioMixer
|
||||||
from .fake import FakeMixer
|
from .fake import FakeMixer
|
||||||
from .nad import NadMixer
|
|
||||||
|
|
||||||
|
|
||||||
def register_mixer(mixer_class):
|
def register_mixer(mixer_class):
|
||||||
@ -19,4 +18,3 @@ def register_mixer(mixer_class):
|
|||||||
def register_mixers():
|
def register_mixers():
|
||||||
register_mixer(AutoAudioMixer)
|
register_mixer(AutoAudioMixer)
|
||||||
register_mixer(FakeMixer)
|
register_mixer(FakeMixer)
|
||||||
register_mixer(NadMixer)
|
|
||||||
|
|||||||
@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
This is Mopidy's default mixer.
|
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
|
from __future__ import unicode_literals
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
"""Fake mixer for use in tests.
|
"""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
|
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
|
audio = None
|
||||||
|
|
||||||
#: The library provider. An instance of
|
#: 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.
|
#: the backend doesn't provide a library.
|
||||||
library = None
|
library = None
|
||||||
|
|
||||||
#: The playback provider. An instance of
|
#: 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.
|
#: the backend doesn't provide playback.
|
||||||
playback = None
|
playback = None
|
||||||
|
|
||||||
#: The playlists provider. An instance of
|
#: 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.
|
#: the backend doesn't provide playlists.
|
||||||
playlists = None
|
playlists = None
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,13 @@ used in tests of the frontends.
|
|||||||
|
|
||||||
The backend handles URIs starting with ``dummy:``.
|
The backend handles URIs starting with ``dummy:``.
|
||||||
|
|
||||||
**Dependencies:**
|
**Dependencies**
|
||||||
|
|
||||||
- None
|
None
|
||||||
|
|
||||||
**Settings:**
|
**Default config**
|
||||||
|
|
||||||
- None
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
@ -22,8 +22,12 @@ from mopidy.backends import base
|
|||||||
from mopidy.models import Playlist, SearchResult
|
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):
|
class DummyBackend(pykka.ThreadingActor, base.Backend):
|
||||||
def __init__(self, audio):
|
def __init__(self, config, audio):
|
||||||
super(DummyBackend, self).__init__()
|
super(DummyBackend, self).__init__()
|
||||||
|
|
||||||
self.library = DummyLibraryProvider(backend=self)
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# flake8: noqa
|
import os
|
||||||
from .actor import LocalBackend
|
|
||||||
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy.backends import base
|
from mopidy.backends import base
|
||||||
|
from mopidy.utils import encoding, path
|
||||||
|
|
||||||
from .library import LocalLibraryProvider
|
from .library import LocalLibraryProvider
|
||||||
from .playlists import LocalPlaylistsProvider
|
from .playlists import LocalPlaylistsProvider
|
||||||
@ -13,11 +15,34 @@ logger = logging.getLogger('mopidy.backends.local')
|
|||||||
|
|
||||||
|
|
||||||
class LocalBackend(pykka.ThreadingActor, base.Backend):
|
class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||||
def __init__(self, audio):
|
def __init__(self, config, audio):
|
||||||
super(LocalBackend, self).__init__()
|
super(LocalBackend, self).__init__()
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.check_dirs_and_files()
|
||||||
|
|
||||||
self.library = LocalLibraryProvider(backend=self)
|
self.library = LocalLibraryProvider(backend=self)
|
||||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||||
|
|
||||||
self.uri_schemes = ['file']
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.backends import base
|
from mopidy.backends import base
|
||||||
from mopidy.models import Album, SearchResult
|
from mopidy.models import Album, SearchResult
|
||||||
|
|
||||||
@ -15,19 +13,24 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||||
self._uri_mapping = {}
|
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()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
def refresh(self, uri=None):
|
||||||
tracks = parse_mpd_tag_cache(
|
logger.debug(
|
||||||
settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH)
|
'Loading local tracks from %s using %s',
|
||||||
|
self._media_dir, self._tag_cache_file)
|
||||||
|
|
||||||
logger.info(
|
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
|
||||||
'Loading tracks from %s using %s',
|
|
||||||
settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE)
|
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
self._uri_mapping[track.uri] = track
|
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):
|
def lookup(self, uri):
|
||||||
try:
|
try:
|
||||||
return [self._uri_mapping[uri]]
|
return [self._uri_mapping[uri]]
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.backends import base, listener
|
from mopidy.backends import base, listener
|
||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist
|
||||||
from mopidy.utils import formatting, path
|
from mopidy.utils import formatting, path
|
||||||
@ -19,7 +18,8 @@ logger = logging.getLogger('mopidy.backends.local')
|
|||||||
class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalPlaylistsProvider, self).__init__(*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()
|
self.refresh()
|
||||||
|
|
||||||
def create(self, name):
|
def create(self, name):
|
||||||
@ -42,16 +42,14 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
|||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
logger.info('Loading playlists from %s', self._path)
|
|
||||||
|
|
||||||
playlists = []
|
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)
|
uri = path.path_to_uri(m3u)
|
||||||
name = os.path.splitext(os.path.basename(m3u))[0]
|
name = os.path.splitext(os.path.basename(m3u))[0]
|
||||||
|
|
||||||
tracks = []
|
tracks = []
|
||||||
for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH):
|
for track_uri in parse_m3u(m3u, self._media_dir):
|
||||||
try:
|
try:
|
||||||
# TODO We must use core.library.lookup() to support tracks
|
# TODO We must use core.library.lookup() to support tracks
|
||||||
# from other backends
|
# from other backends
|
||||||
@ -65,6 +63,10 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
|||||||
self.playlists = playlists
|
self.playlists = playlists
|
||||||
listener.BackendListener.send('playlists_loaded')
|
listener.BackendListener.send('playlists_loaded')
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Loaded %d local playlists from %s',
|
||||||
|
len(playlists), self._playlists_dir)
|
||||||
|
|
||||||
def save(self, playlist):
|
def save(self, playlist):
|
||||||
assert playlist.uri, 'Cannot save playlist without URI'
|
assert playlist.uri, 'Cannot save playlist without URI'
|
||||||
|
|
||||||
@ -86,13 +88,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
|||||||
|
|
||||||
def _get_m3u_path(self, name):
|
def _get_m3u_path(self, name):
|
||||||
name = formatting.slugify(name)
|
name = formatting.slugify(name)
|
||||||
file_path = os.path.join(self._path, name + '.m3u')
|
file_path = os.path.join(self._playlists_dir, name + '.m3u')
|
||||||
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)
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
def _save_m3u(self, playlist):
|
def _save_m3u(self, playlist):
|
||||||
file_path = path.uri_to_path(playlist.uri)
|
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:
|
with open(file_path, 'w') as file_handle:
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
if track.uri.startswith('file://'):
|
if track.uri.startswith('file://'):
|
||||||
@ -103,16 +105,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
|||||||
|
|
||||||
def _delete_m3u(self, uri):
|
def _delete_m3u(self, uri):
|
||||||
file_path = path.uri_to_path(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):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
|
|
||||||
def _rename_m3u(self, playlist):
|
def _rename_m3u(self, playlist):
|
||||||
src_file_path = path.uri_to_path(playlist.uri)
|
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)
|
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)
|
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')
|
logger = logging.getLogger('mopidy.backends.local')
|
||||||
|
|
||||||
|
|
||||||
def parse_m3u(file_path, music_folder):
|
def parse_m3u(file_path, media_dir):
|
||||||
r"""
|
r"""
|
||||||
Convert M3U file list of uris
|
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.
|
- This function does not bother with Extended M3U directives.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO: uris as bytes
|
||||||
uris = []
|
uris = []
|
||||||
try:
|
try:
|
||||||
with open(file_path) as m3u:
|
with open(file_path) as m3u:
|
||||||
@ -49,7 +50,7 @@ def parse_m3u(file_path, music_folder):
|
|||||||
if line.startswith('file://'):
|
if line.startswith('file://'):
|
||||||
uris.append(line)
|
uris.append(line)
|
||||||
else:
|
else:
|
||||||
path = path_to_uri(music_folder, line)
|
path = path_to_uri(media_dir, line)
|
||||||
uris.append(path)
|
uris.append(path)
|
||||||
|
|
||||||
return uris
|
return uris
|
||||||
@ -71,6 +72,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
|||||||
current = {}
|
current = {}
|
||||||
state = None
|
state = None
|
||||||
|
|
||||||
|
# TODO: uris as bytes
|
||||||
for line in contents.split(b'\n'):
|
for line in contents.split(b'\n'):
|
||||||
if line == b'songList begin':
|
if line == b'songList begin':
|
||||||
state = 'songs'
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# flake8: noqa
|
import os
|
||||||
from .actor import SpotifyBackend
|
|
||||||
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.backends import base
|
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')
|
logger = logging.getLogger('mopidy.backends.spotify')
|
||||||
|
|
||||||
|
|
||||||
class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
||||||
# Imports inside methods are to prevent loading of __init__.py to fail on
|
def __init__(self, config, audio):
|
||||||
# missing spotify dependencies.
|
|
||||||
|
|
||||||
def __init__(self, audio):
|
|
||||||
super(SpotifyBackend, self).__init__()
|
super(SpotifyBackend, self).__init__()
|
||||||
|
|
||||||
from .library import SpotifyLibraryProvider
|
self.config = config
|
||||||
from .playback import SpotifyPlaybackProvider
|
|
||||||
from .session_manager import SpotifySessionManager
|
|
||||||
from .playlists import SpotifyPlaylistsProvider
|
|
||||||
|
|
||||||
self.library = SpotifyLibraryProvider(backend=self)
|
self.library = SpotifyLibraryProvider(backend=self)
|
||||||
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
|
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
|
||||||
@ -28,17 +25,8 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
|||||||
|
|
||||||
self.uri_schemes = ['spotify']
|
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(
|
self.spotify = SpotifySessionManager(
|
||||||
username, password, audio=audio, backend_ref=self.actor_ref,
|
config, audio=audio, backend_ref=self.actor_ref)
|
||||||
proxy=proxy, proxy_username=proxy_username,
|
|
||||||
proxy_password=proxy_password)
|
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
logger.info('Mopidy uses SPOTIFY(R) CORE')
|
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
|
import pykka
|
||||||
from spotify import Link, SpotifyError
|
from spotify import Link, SpotifyError
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.backends import base
|
from mopidy.backends import base
|
||||||
from mopidy.models import Track, SearchResult
|
from mopidy.models import Track, SearchResult
|
||||||
|
|
||||||
@ -62,6 +61,10 @@ class SpotifyTrack(Track):
|
|||||||
|
|
||||||
|
|
||||||
class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
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):
|
def find_exact(self, query=None, uris=None):
|
||||||
return self.search(query=query, uris=uris)
|
return self.search(query=query, uris=uris)
|
||||||
|
|
||||||
@ -116,10 +119,11 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
|||||||
SpotifyTrack(track=t)
|
SpotifyTrack(track=t)
|
||||||
for t in playlist if t.availability() == TRACK_AVAILABLE]
|
for t in playlist if t.availability() == TRACK_AVAILABLE]
|
||||||
|
|
||||||
def _wait_for_object_to_load(
|
def _wait_for_object_to_load(self, spotify_obj, timeout=None):
|
||||||
self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT):
|
|
||||||
# XXX Sleeping to wait for the Spotify object to load is an ugly hack,
|
# 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.
|
# but it works. We should look into other solutions for this.
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self._timeout
|
||||||
wait_until = time.time() + timeout
|
wait_until = time.time() + timeout
|
||||||
while not spotify_obj.is_loaded():
|
while not spotify_obj.is_loaded():
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@ -166,7 +170,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
|||||||
future.set(search_result)
|
future.set(search_result)
|
||||||
|
|
||||||
# Wait always returns None on python 2.6 :/
|
# 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():
|
if not self.backend.spotify.connected.is_set():
|
||||||
logger.debug('Not connected: Spotify search cancelled')
|
logger.debug('Not connected: Spotify search cancelled')
|
||||||
return SearchResult(uri='spotify:search')
|
return SearchResult(uri='spotify:search')
|
||||||
@ -176,11 +180,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
|||||||
album_count=200, artist_count=200, track_count=200)
|
album_count=200, artist_count=200, track_count=200)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return future.get(timeout=settings.SPOTIFY_TIMEOUT)
|
return future.get(timeout=self._timeout)
|
||||||
except pykka.Timeout:
|
except pykka.Timeout:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Timeout: Spotify search did not return in %ds',
|
'Timeout: Spotify search did not return in %ds', self._timeout)
|
||||||
settings.SPOTIFY_TIMEOUT)
|
|
||||||
return SearchResult(uri='spotify:search')
|
return SearchResult(uri='spotify:search')
|
||||||
|
|
||||||
def _get_all_tracks(self):
|
def _get_all_tracks(self):
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import threading
|
|||||||
|
|
||||||
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
|
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
|
||||||
|
|
||||||
from mopidy import audio, settings
|
from mopidy import audio
|
||||||
from mopidy.backends.listener import BackendListener
|
from mopidy.backends.listener import BackendListener
|
||||||
from mopidy.utils import process, versioning
|
from mopidy.utils import process, versioning
|
||||||
|
|
||||||
@ -23,17 +23,22 @@ BITRATES = {96: 2, 160: 0, 320: 1}
|
|||||||
|
|
||||||
|
|
||||||
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||||
cache_location = settings.SPOTIFY_CACHE_PATH
|
cache_location = None
|
||||||
settings_location = cache_location
|
settings_location = None
|
||||||
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
||||||
user_agent = 'Mopidy %s' % versioning.get_version()
|
user_agent = 'Mopidy %s' % versioning.get_version()
|
||||||
|
|
||||||
def __init__(self, username, password, audio, backend_ref, proxy=None,
|
def __init__(self, config, audio, backend_ref):
|
||||||
proxy_username=None, proxy_password=None):
|
|
||||||
|
self.cache_location = config['spotify']['cache_dir']
|
||||||
|
self.settings_location = config['spotify']['cache_dir']
|
||||||
|
|
||||||
PyspotifySessionManager.__init__(
|
PyspotifySessionManager.__init__(
|
||||||
self, username, password, proxy=proxy,
|
self, config['spotify']['username'], config['spotify']['password'],
|
||||||
proxy_username=proxy_username,
|
proxy=config['proxy']['hostname'],
|
||||||
proxy_password=proxy_password)
|
proxy_username=config['proxy']['username'],
|
||||||
|
proxy_password=config['proxy']['password'])
|
||||||
|
|
||||||
process.BaseThread.__init__(self)
|
process.BaseThread.__init__(self)
|
||||||
self.name = 'SpotifyThread'
|
self.name = 'SpotifyThread'
|
||||||
|
|
||||||
@ -41,6 +46,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
|||||||
self.backend = None
|
self.backend = None
|
||||||
self.backend_ref = backend_ref
|
self.backend_ref = backend_ref
|
||||||
|
|
||||||
|
self.bitrate = config['spotify']['bitrate']
|
||||||
|
|
||||||
self.connected = threading.Event()
|
self.connected = threading.Event()
|
||||||
self.push_audio_data = True
|
self.push_audio_data = True
|
||||||
self.buffer_timestamp = 0
|
self.buffer_timestamp = 0
|
||||||
@ -66,10 +73,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
|||||||
if not hasattr(self, 'session'):
|
if not hasattr(self, 'session'):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
logger.debug(
|
logger.debug('Preferred Spotify bitrate is %d kbps', self.bitrate)
|
||||||
'Preferred Spotify bitrate is %s kbps',
|
session.set_preferred_bitrate(BITRATES[self.bitrate])
|
||||||
settings.SPOTIFY_BITRATE)
|
|
||||||
session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
|
||||||
|
|
||||||
self.container_manager = SpotifyContainerManager(self)
|
self.container_manager = SpotifyContainerManager(self)
|
||||||
self.playlist_manager = SpotifyPlaylistManager(self)
|
self.playlist_manager = SpotifyPlaylistManager(self)
|
||||||
@ -167,11 +172,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
|||||||
if not self._initial_data_receive_completed:
|
if not self._initial_data_receive_completed:
|
||||||
logger.debug('Still getting data; skipped refresh of playlists')
|
logger.debug('Still getting data; skipped refresh of playlists')
|
||||||
return
|
return
|
||||||
playlists = map(
|
playlists = []
|
||||||
translator.to_mopidy_playlist, self.session.playlist_container())
|
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)
|
playlists = filter(None, playlists)
|
||||||
self.backend.playlists.playlists = 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')
|
BackendListener.send('playlists_loaded')
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
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
|
from mopidy.models import Artist, Album, Track, Playlist
|
||||||
|
|
||||||
|
logger = logging.getLogger('mopidy.backends.spotify')
|
||||||
|
|
||||||
|
|
||||||
artist_cache = {}
|
artist_cache = {}
|
||||||
album_cache = {}
|
album_cache = {}
|
||||||
@ -14,7 +17,7 @@ track_cache = {}
|
|||||||
def to_mopidy_artist(spotify_artist):
|
def to_mopidy_artist(spotify_artist):
|
||||||
if spotify_artist is None:
|
if spotify_artist is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_artist(spotify_artist))
|
uri = str(spotify.Link.from_artist(spotify_artist))
|
||||||
if uri in artist_cache:
|
if uri in artist_cache:
|
||||||
return artist_cache[uri]
|
return artist_cache[uri]
|
||||||
if not spotify_artist.is_loaded():
|
if not spotify_artist.is_loaded():
|
||||||
@ -26,7 +29,7 @@ def to_mopidy_artist(spotify_artist):
|
|||||||
def to_mopidy_album(spotify_album):
|
def to_mopidy_album(spotify_album):
|
||||||
if spotify_album is None:
|
if spotify_album is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_album(spotify_album))
|
uri = str(spotify.Link.from_album(spotify_album))
|
||||||
if uri in album_cache:
|
if uri in album_cache:
|
||||||
return album_cache[uri]
|
return album_cache[uri]
|
||||||
if not spotify_album.is_loaded():
|
if not spotify_album.is_loaded():
|
||||||
@ -39,10 +42,10 @@ def to_mopidy_album(spotify_album):
|
|||||||
return album_cache[uri]
|
return album_cache[uri]
|
||||||
|
|
||||||
|
|
||||||
def to_mopidy_track(spotify_track):
|
def to_mopidy_track(spotify_track, bitrate=None):
|
||||||
if spotify_track is None:
|
if spotify_track is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_track(spotify_track, 0))
|
uri = str(spotify.Link.from_track(spotify_track, 0))
|
||||||
if uri in track_cache:
|
if uri in track_cache:
|
||||||
return track_cache[uri]
|
return track_cache[uri]
|
||||||
if not spotify_track.is_loaded():
|
if not spotify_track.is_loaded():
|
||||||
@ -60,27 +63,31 @@ def to_mopidy_track(spotify_track):
|
|||||||
track_no=spotify_track.index(),
|
track_no=spotify_track.index(),
|
||||||
date=date,
|
date=date,
|
||||||
length=spotify_track.duration(),
|
length=spotify_track.duration(),
|
||||||
bitrate=settings.SPOTIFY_BITRATE)
|
bitrate=bitrate)
|
||||||
return track_cache[uri]
|
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':
|
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
|
||||||
return
|
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():
|
if not spotify_playlist.is_loaded():
|
||||||
return Playlist(uri=uri, name='[loading...]')
|
return Playlist(uri=uri, name='[loading...]')
|
||||||
name = spotify_playlist.name()
|
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:
|
if not name:
|
||||||
# Other user's "starred" playlists isn't handled properly by pyspotify
|
name = 'Starred'
|
||||||
# See https://github.com/mopidy/pyspotify/issues/81
|
# Tracks in the Starred playlist are in reverse order from the official
|
||||||
return
|
# client.
|
||||||
if spotify_playlist.owner().canonical_name() != settings.SPOTIFY_USERNAME:
|
tracks.reverse()
|
||||||
|
if spotify_playlist.owner().canonical_name() != username:
|
||||||
name += ' by ' + spotify_playlist.owner().canonical_name()
|
name += ' by ' + spotify_playlist.owner().canonical_name()
|
||||||
return Playlist(
|
return Playlist(uri=uri, name=name, tracks=tracks)
|
||||||
uri=uri,
|
|
||||||
name=name,
|
|
||||||
tracks=[
|
|
||||||
to_mopidy_track(spotify_track)
|
|
||||||
for spotify_track in spotify_playlist
|
|
||||||
if not spotify_track.is_local()])
|
|
||||||
|
|||||||
@ -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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# flake8: noqa
|
import os
|
||||||
from .actor import StreamBackend
|
|
||||||
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import audio as audio_lib, settings
|
from mopidy import audio as audio_lib
|
||||||
from mopidy.backends import base
|
from mopidy.backends import base
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ logger = logging.getLogger('mopidy.backends.stream')
|
|||||||
|
|
||||||
|
|
||||||
class StreamBackend(pykka.ThreadingActor, base.Backend):
|
class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||||
def __init__(self, audio):
|
def __init__(self, config, audio):
|
||||||
super(StreamBackend, self).__init__()
|
super(StreamBackend, self).__init__()
|
||||||
|
|
||||||
self.library = StreamLibraryProvider(backend=self)
|
self.library = StreamLibraryProvider(backend=self)
|
||||||
@ -21,7 +21,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend):
|
|||||||
self.playlists = None
|
self.playlists = None
|
||||||
|
|
||||||
self.uri_schemes = audio_lib.supported_uri_schemes(
|
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
|
# 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
|
self._message = message
|
||||||
|
|
||||||
|
|
||||||
class SettingsError(MopidyException):
|
class ExtensionError(MopidyException):
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OptionalDependencyError(MopidyException):
|
|
||||||
pass
|
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 @@
|
|||||||
"""
|
from __future__ import unicode_literals
|
||||||
The HTTP frontends lets you control Mopidy through HTTP and WebSockets, e.g.
|
|
||||||
from a web based client.
|
|
||||||
|
|
||||||
**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
|
try:
|
||||||
a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`.
|
import ws4py # noqa
|
||||||
|
except ImportError as e:
|
||||||
|
raise exceptions.ExtensionError('ws4py library not found', e)
|
||||||
|
|
||||||
.. warning:: Security
|
def get_frontend_classes(self):
|
||||||
|
from .actor import HttpFrontend
|
||||||
As a simple security measure, the web server is by default only available
|
return [HttpFrontend]
|
||||||
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
|
|
||||||
|
|||||||
@ -4,18 +4,13 @@ import logging
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
import pykka
|
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
|
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
|
from . import ws
|
||||||
|
|
||||||
|
|
||||||
@ -23,8 +18,9 @@ logger = logging.getLogger('mopidy.frontends.http')
|
|||||||
|
|
||||||
|
|
||||||
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||||
def __init__(self, core):
|
def __init__(self, config, core):
|
||||||
super(HttpFrontend, self).__init__()
|
super(HttpFrontend, self).__init__()
|
||||||
|
self.config = config
|
||||||
self.core = core
|
self.core = core
|
||||||
self._setup_server()
|
self._setup_server()
|
||||||
self._setup_websocket_plugin()
|
self._setup_websocket_plugin()
|
||||||
@ -34,9 +30,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
|||||||
def _setup_server(self):
|
def _setup_server(self):
|
||||||
cherrypy.config.update({
|
cherrypy.config.update({
|
||||||
'engine.autoreload_on': False,
|
'engine.autoreload_on': False,
|
||||||
'server.socket_host': (
|
'server.socket_host': self.config['http']['hostname'],
|
||||||
settings.HTTP_SERVER_HOSTNAME.encode('utf-8')),
|
'server.socket_port': self.config['http']['port'],
|
||||||
'server.socket_port': settings.HTTP_SERVER_PORT,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def _setup_websocket_plugin(self):
|
def _setup_websocket_plugin(self):
|
||||||
@ -48,8 +43,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
|||||||
root.mopidy = MopidyResource()
|
root.mopidy = MopidyResource()
|
||||||
root.mopidy.ws = ws.WebSocketResource(self.core)
|
root.mopidy.ws = ws.WebSocketResource(self.core)
|
||||||
|
|
||||||
if settings.HTTP_SERVER_STATIC_DIR:
|
if self.config['http']['static_dir']:
|
||||||
static_dir = settings.HTTP_SERVER_STATIC_DIR
|
static_dir = self.config['http']['static_dir']
|
||||||
else:
|
else:
|
||||||
static_dir = os.path.join(os.path.dirname(__file__), 'data')
|
static_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||||
logger.debug('HTTP server will serve "%s" at /', static_dir)
|
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
|
import logging
|
||||||
|
|
||||||
from mopidy import core, exceptions, models
|
import cherrypy
|
||||||
from mopidy.utils import jsonrpc
|
from ws4py.websocket import WebSocket
|
||||||
|
|
||||||
try:
|
from mopidy import core, models
|
||||||
import cherrypy
|
from mopidy.utils import jsonrpc
|
||||||
from ws4py.websocket import WebSocket
|
|
||||||
except ImportError as import_error:
|
|
||||||
raise exceptions.OptionalDependencyError(import_error)
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.http')
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# flake8: noqa
|
import os
|
||||||
from .actor import MpdFrontend
|
|
||||||
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.core import CoreListener
|
from mopidy.core import CoreListener
|
||||||
from mopidy.frontends.mpd import session
|
from mopidy.frontends.mpd import session
|
||||||
from mopidy.utils import encoding, network, process
|
from mopidy.utils import encoding, network, process
|
||||||
@ -14,19 +13,23 @@ logger = logging.getLogger('mopidy.frontends.mpd')
|
|||||||
|
|
||||||
|
|
||||||
class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||||
def __init__(self, core):
|
def __init__(self, config, core):
|
||||||
super(MpdFrontend, self).__init__()
|
super(MpdFrontend, self).__init__()
|
||||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
hostname = network.format_hostname(config['mpd']['hostname'])
|
||||||
port = settings.MPD_SERVER_PORT
|
port = config['mpd']['port']
|
||||||
|
|
||||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||||
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
# See https://github.com/mopidy/mopidy/issues/302 for details.
|
||||||
try:
|
try:
|
||||||
network.Server(
|
network.Server(
|
||||||
hostname, port,
|
hostname, port,
|
||||||
protocol=session.MpdSession, protocol_kwargs={b'core': core},
|
protocol=session.MpdSession,
|
||||||
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS,
|
protocol_kwargs={
|
||||||
timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT)
|
b'config': config,
|
||||||
|
b'core': core,
|
||||||
|
},
|
||||||
|
max_connections=config['mpd']['max_connections'],
|
||||||
|
timeout=config['mpd']['connection_timeout'])
|
||||||
except IOError as error:
|
except IOError as error:
|
||||||
logger.error(
|
logger.error(
|
||||||
'MPD server startup failed: %s',
|
'MPD server startup failed: %s',
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import re
|
|||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.frontends.mpd import exceptions, protocol
|
from mopidy.frontends.mpd import exceptions, protocol
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
|
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
|
||||||
@ -22,13 +21,15 @@ class MpdDispatcher(object):
|
|||||||
|
|
||||||
_noidle = re.compile(r'^noidle$')
|
_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.authenticated = False
|
||||||
self.command_list_receiving = False
|
self.command_list_receiving = False
|
||||||
self.command_list_ok = False
|
self.command_list_ok = False
|
||||||
self.command_list = []
|
self.command_list = []
|
||||||
self.command_list_index = None
|
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):
|
def handle_request(self, request, current_command_list_index=None):
|
||||||
"""Dispatch incoming requests to the correct handler."""
|
"""Dispatch incoming requests to the correct handler."""
|
||||||
@ -82,7 +83,7 @@ class MpdDispatcher(object):
|
|||||||
def _authenticate_filter(self, request, response, filter_chain):
|
def _authenticate_filter(self, request, response, filter_chain):
|
||||||
if self.authenticated:
|
if self.authenticated:
|
||||||
return self._call_next_filter(request, response, filter_chain)
|
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
|
self.authenticated = True
|
||||||
return self._call_next_filter(request, response, filter_chain)
|
return self._call_next_filter(request, response, filter_chain)
|
||||||
else:
|
else:
|
||||||
@ -223,6 +224,9 @@ class MpdContext(object):
|
|||||||
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
|
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
|
||||||
session = None
|
session = None
|
||||||
|
|
||||||
|
#: The Mopidy configuration.
|
||||||
|
config = None
|
||||||
|
|
||||||
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
|
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
|
||||||
core = None
|
core = None
|
||||||
|
|
||||||
@ -232,9 +236,55 @@ class MpdContext(object):
|
|||||||
#: The subsytems that we want to be notified about in idle mode.
|
#: The subsytems that we want to be notified about in idle mode.
|
||||||
subscriptions = None
|
subscriptions = None
|
||||||
|
|
||||||
def __init__(self, dispatcher, session=None, core=None):
|
def __init__(self, dispatcher, session=None, config=None, core=None):
|
||||||
self.dispatcher = dispatcher
|
self.dispatcher = dispatcher
|
||||||
self.session = session
|
self.session = session
|
||||||
|
self.config = config
|
||||||
self.core = core
|
self.core = core
|
||||||
self.events = set()
|
self.events = set()
|
||||||
self.subscriptions = 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 __future__ import unicode_literals
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.frontends.mpd.protocol import handle_request
|
from mopidy.frontends.mpd.protocol import handle_request
|
||||||
from mopidy.frontends.mpd.exceptions import (
|
from mopidy.frontends.mpd.exceptions import (
|
||||||
MpdPasswordError, MpdPermissionError)
|
MpdPasswordError, MpdPermissionError)
|
||||||
@ -40,7 +39,7 @@ def password_(context, password):
|
|||||||
This is used for authentication with the server. ``PASSWORD`` is
|
This is used for authentication with the server. ``PASSWORD`` is
|
||||||
simply the plaintext password.
|
simply the plaintext password.
|
||||||
"""
|
"""
|
||||||
if password == settings.MPD_SERVER_PASSWORD:
|
if password == context.config['mpd']['password']:
|
||||||
context.dispatcher.authenticated = True
|
context.dispatcher.authenticated = True
|
||||||
else:
|
else:
|
||||||
raise MpdPasswordError('incorrect password', command='password')
|
raise MpdPasswordError('incorrect password', command='password')
|
||||||
|
|||||||
@ -381,10 +381,8 @@ def searchaddpl(context, playlist_name, mpd_query):
|
|||||||
return
|
return
|
||||||
results = context.core.library.search(**query).get()
|
results = context.core.library.search(**query).get()
|
||||||
|
|
||||||
playlists = context.core.playlists.filter(name=playlist_name).get()
|
playlist = context.lookup_playlist_from_name(playlist_name)
|
||||||
if playlists:
|
if not playlist:
|
||||||
playlist = playlists[0]
|
|
||||||
else:
|
|
||||||
playlist = context.core.playlists.create(playlist_name).get()
|
playlist = context.core.playlists.create(playlist_name).get()
|
||||||
tracks = list(playlist.tracks) + _get_tracks(results)
|
tracks = list(playlist.tracks) + _get_tracks(results)
|
||||||
playlist = playlist.copy(tracks=tracks)
|
playlist = playlist.copy(tracks=tracks)
|
||||||
|
|||||||
@ -23,10 +23,10 @@ def listplaylist(context, name):
|
|||||||
file: relative/path/to/file2.ogg
|
file: relative/path/to/file2.ogg
|
||||||
file: relative/path/to/file3.mp3
|
file: relative/path/to/file3.mp3
|
||||||
"""
|
"""
|
||||||
playlists = context.core.playlists.filter(name=name).get()
|
playlist = context.lookup_playlist_from_name(name)
|
||||||
if not playlists:
|
if not playlist:
|
||||||
raise MpdNoExistError('No such playlist', command='listplaylist')
|
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+)$')
|
@handle_request(r'^listplaylistinfo (?P<name>\w+)$')
|
||||||
@ -44,10 +44,10 @@ def listplaylistinfo(context, name):
|
|||||||
Standard track listing, with fields: file, Time, Title, Date,
|
Standard track listing, with fields: file, Time, Title, Date,
|
||||||
Album, Artist, Track
|
Album, Artist, Track
|
||||||
"""
|
"""
|
||||||
playlists = context.core.playlists.filter(name=name).get()
|
playlist = context.lookup_playlist_from_name(name)
|
||||||
if not playlists:
|
if not playlist:
|
||||||
raise MpdNoExistError('No such playlist', command='listplaylistinfo')
|
raise MpdNoExistError('No such playlist', command='listplaylistinfo')
|
||||||
return playlist_to_mpd_format(playlists[0])
|
return playlist_to_mpd_format(playlist)
|
||||||
|
|
||||||
|
|
||||||
@handle_request(r'^listplaylists$')
|
@handle_request(r'^listplaylists$')
|
||||||
@ -80,7 +80,8 @@ def listplaylists(context):
|
|||||||
for playlist in context.core.playlists.playlists.get():
|
for playlist in context.core.playlists.playlists.get():
|
||||||
if not playlist.name:
|
if not playlist.name:
|
||||||
continue
|
continue
|
||||||
result.append(('playlist', playlist.name))
|
name = context.lookup_playlist_name_from_uri(playlist.uri)
|
||||||
|
result.append(('playlist', name))
|
||||||
last_modified = (
|
last_modified = (
|
||||||
playlist.last_modified or dt.datetime.utcnow()).isoformat()
|
playlist.last_modified or dt.datetime.utcnow()).isoformat()
|
||||||
# Remove microseconds
|
# 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,
|
- MPD 0.17.1 does not fail if the specified range is outside the playlist,
|
||||||
in either or both ends.
|
in either or both ends.
|
||||||
"""
|
"""
|
||||||
playlists = context.core.playlists.filter(name=name).get()
|
playlist = context.lookup_playlist_from_name(name)
|
||||||
if not playlists:
|
if not playlist:
|
||||||
raise MpdNoExistError('No such playlist', command='load')
|
raise MpdNoExistError('No such playlist', command='load')
|
||||||
if start is not None:
|
if start is not None:
|
||||||
start = int(start)
|
start = int(start)
|
||||||
if end is not None:
|
if end is not None:
|
||||||
end = int(end)
|
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>[^"]+)"$')
|
@handle_request(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||||
|
|||||||
@ -18,9 +18,10 @@ class MpdSession(network.LineProtocol):
|
|||||||
encoding = protocol.ENCODING
|
encoding = protocol.ENCODING
|
||||||
delimiter = r'\r?\n'
|
delimiter = r'\r?\n'
|
||||||
|
|
||||||
def __init__(self, connection, core=None):
|
def __init__(self, connection, config=None, core=None):
|
||||||
super(MpdSession, self).__init__(connection)
|
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):
|
def on_start(self):
|
||||||
logger.info('New MPD connection from [%s]:%s', self.host, self.port)
|
logger.info('New MPD connection from [%s]:%s', self.host, self.port)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import re
|
|||||||
import shlex
|
import shlex
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.frontends.mpd import protocol
|
from mopidy.frontends.mpd import protocol
|
||||||
from mopidy.frontends.mpd.exceptions import MpdArgError
|
from mopidy.frontends.mpd.exceptions import MpdArgError
|
||||||
from mopidy.models import TlTrack
|
from mopidy.models import TlTrack
|
||||||
@ -216,12 +215,14 @@ def query_from_mpd_search_format(mpd_query):
|
|||||||
return 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
|
Format list of tracks for output to MPD tag cache
|
||||||
|
|
||||||
:param tracks: the tracks
|
:param tracks: the tracks
|
||||||
:type tracks: list of :class:`mopidy.models.Track`
|
: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
|
:rtype: list of lists of two-tuples
|
||||||
"""
|
"""
|
||||||
result = [
|
result = [
|
||||||
@ -231,14 +232,15 @@ def tracks_to_tag_cache_format(tracks):
|
|||||||
('info_end',)
|
('info_end',)
|
||||||
]
|
]
|
||||||
tracks.sort(key=lambda t: t.uri)
|
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
|
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):
|
for path, (entry_dirs, entry_files) in dirs.items():
|
||||||
base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8')
|
|
||||||
|
|
||||||
for path, entry in folders.items():
|
|
||||||
try:
|
try:
|
||||||
text_path = path.decode('utf-8')
|
text_path = path.decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
@ -247,7 +249,7 @@ def _add_to_tag_cache(result, folders, files):
|
|||||||
result.append(('directory', text_path))
|
result.append(('directory', text_path))
|
||||||
result.append(('mtime', get_mtime(os.path.join(base_path, path))))
|
result.append(('mtime', get_mtime(os.path.join(base_path, path))))
|
||||||
result.append(('begin', name))
|
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(('end', name))
|
||||||
|
|
||||||
result.append(('songList begin',))
|
result.append(('songList begin',))
|
||||||
@ -273,7 +275,7 @@ def _add_to_tag_cache(result, folders, files):
|
|||||||
result.append(('songList end',))
|
result.append(('songList end',))
|
||||||
|
|
||||||
|
|
||||||
def tracks_to_directory_tree(tracks):
|
def tracks_to_directory_tree(tracks, media_dir):
|
||||||
directories = ({}, [])
|
directories = ({}, [])
|
||||||
|
|
||||||
for track in tracks:
|
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))
|
absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri))
|
||||||
relative_track_dir_path = re.sub(
|
relative_track_dir_path = re.sub(
|
||||||
'^' + re.escape(settings.LOCAL_MUSIC_PATH), b'',
|
'^' + re.escape(media_dir), b'', absolute_track_dir_path)
|
||||||
absolute_track_dir_path)
|
|
||||||
|
|
||||||
for part in split_path(relative_track_dir_path):
|
for part in split_path(relative_track_dir_path):
|
||||||
path = os.path.join(path, part)
|
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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# flake8: noqa
|
import os
|
||||||
from .actor import MprisFrontend
|
|
||||||
|
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