Release v0.14.0

This commit is contained in:
Stein Magnus Jodal 2013-04-28 21:49:57 +02:00
commit 027b0e2e8c
166 changed files with 5509 additions and 3993 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.egg-info
*.pyc *.pyc
*.swp *.swp
.coverage .coverage

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.__main__ import main
main()

View File

@ -1,5 +0,0 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.scanner import main
main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,139 @@
.. _contributing:
************
Contributing
************
If you are thinking about making Mopidy better, or you just want to hack on it,
thats 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/>`_

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
*********************************************************
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
*********************************************************
.. automodule:: mopidy.backends.dummy
:synopsis: Dummy backend used for testing
:members:

View File

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

View File

@ -1,8 +0,0 @@
.. _spotify-backend:
*************************************************
:mod:`mopidy.backends.spotify` -- Spotify backend
*************************************************
.. automodule:: mopidy.backends.spotify
:synopsis: Backend for the Spotify music streaming service

View File

@ -1,7 +0,0 @@
***********************************************
:mod:`mopidy.backends.stream` -- Stream backend
***********************************************
.. automodule:: mopidy.backends.stream
:synopsis: Backend for playing audio streams
:members:

View File

@ -1,8 +0,0 @@
.. _http-frontend:
*********************************************
:mod:`mopidy.frontends.http` -- HTTP frontend
*********************************************
.. automodule:: mopidy.frontends.http
:synopsis: HTTP and WebSockets frontend

View File

@ -1,6 +0,0 @@
***************************************************
:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
***************************************************
.. automodule:: mopidy.frontends.lastfm
:synopsis: Last.fm scrobbler frontend

View File

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

View File

@ -1,8 +0,0 @@
.. _mpris-frontend:
***********************************************
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
***********************************************
.. automodule:: mopidy.frontends.mpris
:synopsis: MPRIS frontend

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
[spotify]
enabled = true
username =
password =
bitrate = 160
timeout = 10
cache_dir = $XDG_CACHE_DIR/mopidy/spotify

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
[stream]
enabled = true
protocols =
http
https
mms
rtmp
rtmps
rtsp

166
mopidy/config/__init__.py Normal file
View 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
View 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.'

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
[http]
enabled = true
hostname = 127.0.0.1
port = 6680
static_dir =
[loglevels]
cherrypy = warning

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
[mpd]
enabled = true
hostname = 127.0.0.1
port = 6600
password =
max_connections = 20
connection_timeout = 60

View File

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

View File

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

View File

@ -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>[^"]+)"$')

View File

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

View File

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

View File

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