Release v0.19.0
7
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
*.egg-info
|
||||
*.orig
|
||||
*.pyc
|
||||
*.swp
|
||||
*~
|
||||
.coverage
|
||||
.idea
|
||||
.noseids
|
||||
@ -11,9 +13,8 @@ cover/
|
||||
coverage.xml
|
||||
dist/
|
||||
docs/_build/
|
||||
js/test/lib/
|
||||
mopidy.log*
|
||||
node_modules/
|
||||
nosetests.xml
|
||||
*~
|
||||
*.orig
|
||||
js/test/lib/
|
||||
xunit-*.xml
|
||||
|
||||
1
.mailmap
@ -12,5 +12,6 @@ Javier Domingo Cansino <javierdo1@gmail.com> <javier.domingo@fon.com>
|
||||
Lasse Bigum <lasse@bigum.org> <l.bigum@samsung.com>
|
||||
Nick Steel <kingosticks@gmail.com> <kingosticks@gmail.com>
|
||||
Janez Troha <janez.troha@gmail.com> <dz0ny@users.noreply.github.com>
|
||||
Janez Troha <janez.troha@gmail.com> <dz0ny@ubuntu.si>
|
||||
Luke Giuliani <luke@giuliani.com.au>
|
||||
Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
|
||||
20
.travis.yml
@ -1,21 +1,25 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7_with_system_site_packages"
|
||||
|
||||
env:
|
||||
- TOX_ENV=py27
|
||||
- TOX_ENV=docs
|
||||
- TOX_ENV=flake8
|
||||
|
||||
install:
|
||||
- "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
|
||||
- "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
|
||||
- "sudo apt-get update || true"
|
||||
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy|python:any/ {print $2}')"
|
||||
- "pip install coveralls flake8"
|
||||
|
||||
before_script:
|
||||
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
|
||||
- "sudo apt-get install mopidy graphviz-dev"
|
||||
- "pip install tox"
|
||||
|
||||
script:
|
||||
- "flake8 $(find . -iname '*.py')"
|
||||
- "nosetests --with-coverage --cover-package=mopidy"
|
||||
- "tox -e $TOX_ENV"
|
||||
|
||||
after_success:
|
||||
- "coveralls"
|
||||
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
||||
2
AUTHORS
@ -38,3 +38,5 @@
|
||||
- Arnaud Barisain-Monrose <abarisain@gmail.com>
|
||||
- nathanharper <nathan.sam.harper@gmail.com>
|
||||
- Pierpaolo Frasa <pfrasa@smail.uni-koeln.de>
|
||||
- Thomas Scholtes <thomas-scholtes@gmx.de>
|
||||
- Sam Willcocks <sam@wlcx.cc>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
include *.py
|
||||
include *.rst
|
||||
include *.txt
|
||||
include .coveragerc
|
||||
include .mailmap
|
||||
include .travis.yml
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include tox.ini
|
||||
|
||||
recursive-include data *
|
||||
|
||||
|
||||
10
README.rst
@ -19,24 +19,24 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
- `Source code <https://github.com/mopidy/mopidy>`_
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy/archive/develop.tar.gz#egg=mopidy-dev>`_
|
||||
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
|
||||
|
||||
.. image:: https://pypip.in/v/Mopidy/badge.png
|
||||
.. image:: https://img.shields.io/pypi/v/Mopidy.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/Mopidy/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
.. image:: https://pypip.in/d/Mopidy/badge.png
|
||||
.. image:: https://img.shields.io/pypi/dm/Mopidy.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/Mopidy/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
.. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat
|
||||
:target: https://travis-ci.org/mopidy/mopidy
|
||||
:alt: Travis CI build status
|
||||
|
||||
.. image:: https://coveralls.io/repos/mopidy/mopidy/badge.png?branch=develop
|
||||
.. image:: https://img.shields.io/coveralls/mopidy/mopidy/develop.svg?style=flat
|
||||
:target: https://coveralls.io/r/mopidy/mopidy?branch=develop
|
||||
:alt: Test coverage
|
||||
|
||||
28
dev-requirements.txt
Normal file
@ -0,0 +1,28 @@
|
||||
# Automate tasks
|
||||
fabric
|
||||
|
||||
# Build documentation
|
||||
sphinx
|
||||
|
||||
# Check code style, errors, etc
|
||||
flake8
|
||||
flake8-import-order
|
||||
|
||||
# Mock dependencies in tests
|
||||
mock
|
||||
|
||||
# Test runners
|
||||
nose
|
||||
tox
|
||||
|
||||
# Measure test's code coverage
|
||||
coverage
|
||||
|
||||
# Check that MANIFEST.in matches Git repo contents before making a release
|
||||
check-manifest
|
||||
|
||||
# To make wheel packages
|
||||
wheel
|
||||
|
||||
# Securely upload packages to PyPI
|
||||
twine
|
||||
@ -93,27 +93,7 @@ Backend listener
|
||||
:members:
|
||||
|
||||
|
||||
.. _backend-implementations:
|
||||
|
||||
Backend implementations
|
||||
=======================
|
||||
|
||||
- `Mopidy-Beets <https://github.com/mopidy/mopidy-beets>`_
|
||||
|
||||
- `Mopidy-GMusic <https://github.com/hechtus/mopidy-gmusic>`_
|
||||
|
||||
- :ref:`ext-local`
|
||||
|
||||
- `Mopidy-radio-de <https://github.com/hechtus/mopidy-radio-de>`_
|
||||
|
||||
- `Mopidy-SomaFM <https://github.com/AlexandrePTJ/mopidy-somafm>`_
|
||||
|
||||
- `Mopidy-SoundCloud <https://github.com/mopidy/mopidy-soundcloud>`_
|
||||
|
||||
- `Mopidy-Spotify <https://github.com/mopidy/mopidy-spotify>`_
|
||||
|
||||
- :ref:`ext-stream`
|
||||
|
||||
- `Mopidy-Subsonic <https://github.com/rattboi/mopidy-subsonic>`_
|
||||
|
||||
- `Mopidy-VKontakte <https://github.com/sibuser/mopidy-vkontakte>`_
|
||||
See :ref:`ext-backends`.
|
||||
|
||||
@ -29,12 +29,12 @@ The following requirements applies to any frontend implementation:
|
||||
- The main actor MUST be able to start and stop the frontend when the main
|
||||
actor is started and stopped.
|
||||
|
||||
- The frontend MAY require additional settings to be set for it to
|
||||
work.
|
||||
- The frontend MAY require additional config values to be set for it to work.
|
||||
|
||||
- Such settings MUST be documented.
|
||||
- Such config values MUST be documented.
|
||||
|
||||
- The main actor MUST stop itself if the defined settings are not adequate for
|
||||
- The main actor MUST raise the :exc:`mopidy.exceptions.FrontendError` with a
|
||||
descriptive error message if the defined config values are not adequate for
|
||||
the frontend to work properly.
|
||||
|
||||
- Any actor which is part of the frontend MAY implement the
|
||||
@ -42,17 +42,7 @@ The following requirements applies to any frontend implementation:
|
||||
specified events.
|
||||
|
||||
|
||||
.. _frontend-implementations:
|
||||
|
||||
Frontend implementations
|
||||
========================
|
||||
|
||||
- :ref:`ext-http`
|
||||
|
||||
- :ref:`ext-mpd`
|
||||
|
||||
- `Mopidy-MPRIS <https://github.com/mopidy/mopidy-mpris>`_
|
||||
|
||||
- `Mopidy-Notifier <https://github.com/sauberfred/mopidy-notifier>`_
|
||||
|
||||
- `Mopidy-Scrobbler <https://github.com/mopidy/mopidy-scrobbler>`_
|
||||
See :ref:`ext-frontends`.
|
||||
|
||||
195
docs/api/http-server.rst
Normal file
@ -0,0 +1,195 @@
|
||||
.. _http-server-api:
|
||||
|
||||
********************
|
||||
HTTP server side API
|
||||
********************
|
||||
|
||||
The :ref:`ext-http` extension comes with an HTTP server to host Mopidy's
|
||||
:ref:`http-api`. This web server can also be used by other extensions that need
|
||||
to expose something over HTTP.
|
||||
|
||||
The HTTP server side API can be used to:
|
||||
|
||||
- host static files for e.g. a Mopidy client written in pure JavaScript,
|
||||
- host a `Tornado <http://www.tornadoweb.org/>`__ application, or
|
||||
- host a WSGI application, including e.g. Flask applications.
|
||||
|
||||
To host static files using the web server, an extension needs to register a
|
||||
name and a file path in the extension registry under the ``http:static`` key.
|
||||
|
||||
To extend the web server with a web application, an extension must register a
|
||||
name and a factory function in the extension registry under the ``http:app``
|
||||
key.
|
||||
|
||||
For details on how to make a Mopidy extension, see the :ref:`extensiondev`
|
||||
guide.
|
||||
|
||||
|
||||
Static web client example
|
||||
=========================
|
||||
|
||||
To serve static files, you just need to register an ``http:static`` dictionary
|
||||
in the extension registry. The dictionary must have two keys: ``name`` and
|
||||
``path``. The ``name`` is used to build the URL the static files will be
|
||||
served on. By convention, it should be identical with the extension's
|
||||
:attr:`~mopidy.ext.Extension.ext_name`, like in the following example. The
|
||||
``path`` tells Mopidy where on the disk the static files are located.
|
||||
|
||||
Assuming that the code below is located in the file
|
||||
:file:`mywebclient/__init__.py`, the files in the directory
|
||||
:file:`mywebclient/static/` will be made available at ``/mywebclient/`` on
|
||||
Mopidy's web server. For example, :file:`mywebclient/static/foo.html` will be
|
||||
available at http://localhost:6680/mywebclient/foo.html.
|
||||
|
||||
::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from mopidy import ext
|
||||
|
||||
|
||||
class MyWebClientExtension(ext.Extension):
|
||||
ext_name = 'mywebclient'
|
||||
|
||||
def setup(self, registry):
|
||||
registry.add('http:static', {
|
||||
'name': self.ext_name,
|
||||
'path': os.path.join(os.path.dirname(__file__), 'static'),
|
||||
})
|
||||
|
||||
# See the Extension API for the full details on this class
|
||||
|
||||
|
||||
Tornado application example
|
||||
===========================
|
||||
|
||||
The :ref:`ext-http` extension's web server is based on the `Tornado
|
||||
<http://www.tornadoweb.org/>`__ web framework. Thus, it has first class support
|
||||
for Tornado request handlers.
|
||||
|
||||
In the following example, we create a :class:`tornado.web.RequestHandler`
|
||||
called :class:`MyRequestHandler` that responds to HTTP GET requests with the
|
||||
string ``Hello, world! This is Mopidy $version``, where it gets the Mopidy
|
||||
version from Mopidy's core API.
|
||||
|
||||
To hook the request handler into Mopidy's web server, we must register a
|
||||
dictionary under the ``http:app`` key in the extension registry. The
|
||||
dictionary must have two keys: ``name`` and ``factory``.
|
||||
|
||||
The ``name`` is used to build the URL the app will be served on. By convention,
|
||||
it should be identical with the extension's
|
||||
:attr:`~mopidy.ext.Extension.ext_name`, like in the following example.
|
||||
|
||||
The ``factory`` must be a function that accepts two arguments, ``config`` and
|
||||
``core``, respectively a dict structure of Mopidy's config and a
|
||||
:class:`pykka.ActorProxy` to the full Mopidy core API. The ``factory`` function
|
||||
must return a list of Tornado request handlers. The URL patterns of the request
|
||||
handlers should not include the ``name``, as that will be prepended to the URL
|
||||
patterns by the web server.
|
||||
|
||||
When the extension is installed, Mopidy will respond to requests to
|
||||
http://localhost:6680/mywebclient/ with the string ``Hello, world! This is
|
||||
Mopidy $version``.
|
||||
|
||||
::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import tornado.web
|
||||
|
||||
from mopidy import ext
|
||||
|
||||
|
||||
class MyRequestHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, core):
|
||||
self.core = core
|
||||
|
||||
def get(self):
|
||||
self.write(
|
||||
'Hello, world! This is Mopidy %s' %
|
||||
self.core.get_version().get())
|
||||
|
||||
|
||||
def my_app_factory(config, core):
|
||||
return [
|
||||
('/', MyRequestHandler, {'core': core})
|
||||
]
|
||||
|
||||
|
||||
class MyWebClientExtension(ext.Extension):
|
||||
ext_name = 'mywebclient'
|
||||
|
||||
def setup(self, registry):
|
||||
registry.add('http:app', {
|
||||
'name': self.ext_name,
|
||||
'factory': my_app_factory,
|
||||
})
|
||||
|
||||
# See the Extension API for the full details on this class
|
||||
|
||||
|
||||
|
||||
WSGI application example
|
||||
========================
|
||||
|
||||
WSGI applications are second-class citizens on Mopidy's HTTP server. The WSGI
|
||||
applications are run inside Tornado, which is based on non-blocking I/O and a
|
||||
single event loop. In other words, your WSGI applications will only have a
|
||||
single thread to run on, and if your application is doing blocking I/O, it will
|
||||
block all other requests from being handled by the web server as well.
|
||||
|
||||
The example below shows how a WSGI application that returns the string
|
||||
``Hello, world! This is Mopidy $version`` on all requests. The WSGI application
|
||||
is wrapped as a Tornado application and mounted at
|
||||
http://localhost:6680/mywebclient/.
|
||||
|
||||
::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import tornado.web
|
||||
import tornado.wsgi
|
||||
|
||||
from mopidy import ext
|
||||
|
||||
|
||||
def my_app_factory(config, core):
|
||||
|
||||
def wsgi_app(environ, start_response):
|
||||
status = '200 OK'
|
||||
response_headers = [('Content-type', 'text/plain')]
|
||||
start_response(status, response_headers)
|
||||
return [
|
||||
'Hello, world! This is Mopidy %s\n' %
|
||||
self.core.get_version().get()
|
||||
]
|
||||
|
||||
return [
|
||||
('(.*)', tornado.web.FallbackHandler, {
|
||||
'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
|
||||
}),
|
||||
]
|
||||
|
||||
|
||||
class MyWebClientExtension(ext.Extension):
|
||||
ext_name = 'mywebclient'
|
||||
|
||||
def setup(self, registry):
|
||||
registry.add('http:app', {
|
||||
'name': self.ext_name,
|
||||
'factory': my_app_factory,
|
||||
})
|
||||
|
||||
# See the Extension API for the full details on this class
|
||||
|
||||
|
||||
API implementors
|
||||
================
|
||||
|
||||
See :ref:`ext-web`.
|
||||
@ -1,18 +1,23 @@
|
||||
.. _http-api:
|
||||
|
||||
********
|
||||
HTTP API
|
||||
********
|
||||
*****************
|
||||
HTTP JSON-RPC API
|
||||
*****************
|
||||
|
||||
The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available over
|
||||
HTTP using WebSockets. We also provide a JavaScript wrapper, called
|
||||
:ref:`Mopidy.js <mopidy-js>` around the HTTP API for use both from browsers and
|
||||
Node.js.
|
||||
.. module:: mopidy.http
|
||||
:synopsis: The HTTP frontend APIs
|
||||
|
||||
The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available using
|
||||
JSON-RPC over HTTP using HTTP POST and WebSockets. We also provide a JavaScript
|
||||
wrapper, called :ref:`Mopidy.js <mopidy-js>`, around the JSON-RPC over
|
||||
WebSocket API for use both from browsers and Node.js. The
|
||||
:ref:`http-explore-extension` extension, can also be used to get you
|
||||
familiarized with HTTP based APIs.
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Since the HTTP API exposes our internal core API directly it is to be
|
||||
regarded as **experimental**. We cannot promise to keep any form of
|
||||
Since the HTTP JSON-RPC API exposes our internal core API directly it is to
|
||||
be regarded as **experimental**. We cannot promise to keep any form of
|
||||
backwards compatibility between releases as we will need to change the core
|
||||
API while working out how to support new use cases. Thus, if you use this
|
||||
API, you must expect to do small adjustments to your client for every
|
||||
@ -22,36 +27,50 @@ Node.js.
|
||||
stable.
|
||||
|
||||
|
||||
.. _http-post-api:
|
||||
|
||||
HTTP POST API
|
||||
=============
|
||||
|
||||
The Mopidy web server accepts HTTP requests with the POST method to
|
||||
http://localhost:6680/mopidy/rpc, where the ``localhost:6680`` part will vary
|
||||
with your local setup. The HTTP POST endpoint gives you access to Mopidy's
|
||||
full core API, but does not give you notification on events. If you need
|
||||
to listen to events, you should probably use the WebSocket API instead.
|
||||
|
||||
Example usage from the command line::
|
||||
|
||||
$ curl -d '{"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_state"}' http://localhost:6680/mopidy/rpc
|
||||
{"jsonrpc": "2.0", "id": 1, "result": "stopped"}
|
||||
|
||||
For details on the request and response format, see :ref:`json-rpc`.
|
||||
|
||||
|
||||
.. _websocket-api:
|
||||
|
||||
WebSocket API
|
||||
=============
|
||||
|
||||
The web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you
|
||||
access to Mopidy's full API and enables Mopidy to instantly push events to the
|
||||
client, as they happen.
|
||||
The Mopidy web server exposes a WebSocket at http://localhost:6680/mopidy/ws,
|
||||
where the ``localhost:6680`` part will vary with your local setup. The
|
||||
WebSocket gives you access to Mopidy's full API and enables Mopidy to instantly
|
||||
push events to the client, as they happen.
|
||||
|
||||
On the WebSocket we send two different kind of messages: The client can send
|
||||
JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses.
|
||||
In addition, the server will send event messages when something happens on the
|
||||
server. Both message types are encoded as JSON objects.
|
||||
:ref:`JSON-RPC 2.0 requests <json-rpc>`, and the server will respond with
|
||||
JSON-RPC 2.0 responses. In addition, the server will send :ref:`event messages
|
||||
<json-events>` when something happens on the server. Both message types are
|
||||
encoded as JSON objects.
|
||||
|
||||
If you're using the API from JavaScript, either in the browser or in Node.js,
|
||||
you should use :ref:`mopidy-js` which wraps the WebSocket API in a nice
|
||||
JavaScript API.
|
||||
|
||||
|
||||
Event messages
|
||||
--------------
|
||||
.. _json-rpc:
|
||||
|
||||
Event objects will always have a key named ``event`` whose value is the event
|
||||
type. Depending on the event type, the event may include additional fields for
|
||||
related data. The events maps directly to the :class:`mopidy.core.CoreListener`
|
||||
API. Refer to the ``CoreListener`` method names is the available event types.
|
||||
The ``CoreListener`` method's keyword arguments are all included as extra
|
||||
fields on the event objects. Example event message::
|
||||
|
||||
{"event": "track_playback_started", "track": {...}}
|
||||
|
||||
|
||||
JSON-RPC 2.0 messaging
|
||||
----------------------
|
||||
JSON-RPC 2.0 messages
|
||||
=====================
|
||||
|
||||
JSON-RPC 2.0 messages can be recognized by checking for the key named
|
||||
``jsonrpc`` with the string value ``2.0``. For details on the messaging format,
|
||||
@ -80,360 +99,17 @@ available methods. If you're unsure how the core API maps to JSON-RPC, having a
|
||||
look at the ``core.describe`` response can be helpful.
|
||||
|
||||
|
||||
.. _mopidy-js:
|
||||
.. _json-events:
|
||||
|
||||
Mopidy.js JavaScript library
|
||||
============================
|
||||
Event messages
|
||||
==============
|
||||
|
||||
We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets
|
||||
you quickly started with working on your client instead of figuring out how to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting the library for browser use
|
||||
-----------------------------------
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP extension is enabled, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
Thus, if you use Mopidy to host your web client, like described above, you can
|
||||
load the latest version of Mopidy.js by adding the following script tag to your
|
||||
HTML file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
If you don't use Mopidy to host your web client, you can find the JS files in
|
||||
the Git repo at:
|
||||
|
||||
- ``mopidy/http/data/mopidy.js``
|
||||
- ``mopidy/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Getting the library for Node.js use
|
||||
-----------------------------------
|
||||
|
||||
If you want to use Mopidy.js from Node.js instead of a browser, you can install
|
||||
Mopidy.js using npm::
|
||||
|
||||
npm install mopidy
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Getting the library for development on the library
|
||||
--------------------------------------------------
|
||||
|
||||
If you want to work on the Mopidy.js library itself, you'll find a complete
|
||||
development setup in the ``js/`` dir in our repo. The instructions in
|
||||
``js/README.md`` will guide you on your way.
|
||||
|
||||
|
||||
Creating an instance
|
||||
--------------------
|
||||
|
||||
Once you got Mopidy.js loaded, you need to create an instance of the wrapper:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy();
|
||||
|
||||
When you instantiate ``Mopidy()`` without arguments, it will connect to
|
||||
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
|
||||
your web client using Mopidy's web server, or if you use Mopidy.js from a
|
||||
Node.js environment, you'll need to pass the URL to the WebSocket end point:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy({
|
||||
webSocketUrl: "ws://localhost:6680/mopidy/ws/"
|
||||
});
|
||||
|
||||
It is also possible to create an instance first and connect to the WebSocket
|
||||
later:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy({autoConnect: false});
|
||||
// ... do other stuff, like hooking up events ...
|
||||
mopidy.connect();
|
||||
|
||||
|
||||
Hooking up to events
|
||||
--------------------
|
||||
|
||||
Once you have a Mopidy.js object, you can hook up to the events it emits. To
|
||||
explore your possibilities, it can be useful to subscribe to all events and log
|
||||
them:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on(console.log.bind(console));
|
||||
|
||||
Several types of events are emitted:
|
||||
|
||||
- You can get notified about when the Mopidy.js object is connected to the
|
||||
server and ready for method calls, when it's offline, and when it's trying to
|
||||
reconnect to the server by looking at the events ``state:online``,
|
||||
``state:offline``, ``reconnectionPending``, and ``reconnecting``.
|
||||
|
||||
- You can get events sent from the Mopidy server by looking at the events with
|
||||
the name prefix ``event:``, like ``event:trackPlaybackStarted``.
|
||||
|
||||
- You can introspect what happens internally on the WebSocket by looking at the
|
||||
events emitted with the name prefix ``websocket:``.
|
||||
|
||||
Mopidy.js uses the event emitter library `BANE
|
||||
<https://github.com/busterjs/bane>`_, so you should refer to BANE's
|
||||
short API documentation to see how you can hook up your listeners to the
|
||||
different events.
|
||||
|
||||
|
||||
Calling core API methods
|
||||
------------------------
|
||||
|
||||
Once your Mopidy.js object has connected to the Mopidy server and emits the
|
||||
``state:online`` event, it is ready to accept core API method calls:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on("state:online", function () {
|
||||
mopidy.playback.next();
|
||||
});
|
||||
|
||||
Any calls you make before the ``state:online`` event is emitted will fail. If
|
||||
you've hooked up an errback (more on that a bit later) to the promise returned
|
||||
from the call, the errback will be called with an error message.
|
||||
|
||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
|
||||
API attributes is *not* available, but that shouldn't be a problem as we've
|
||||
added (undocumented) getters and setters for all of them, so you can access the
|
||||
attributes as well from JavaScript.
|
||||
|
||||
Both the WebSocket API and the JavaScript API are based on introspection of the
|
||||
core Python API. Thus, they will always be up to date and immediately reflect
|
||||
any changes we do to the core API.
|
||||
|
||||
The best way to explore the JavaScript API, is probably by opening your
|
||||
browser's console, and using its tab completion to navigate the API. You'll
|
||||
find the Mopidy core API exposed under ``mopidy.playback``,
|
||||
``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``.
|
||||
|
||||
All methods in the JavaScript API have an associated data structure describing
|
||||
the Python params it expects, and most methods also have the Python API
|
||||
documentation available. This is available right there in the browser console,
|
||||
by looking at the method's ``description`` and ``params`` attributes:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
console.log(mopidy.playback.next.params);
|
||||
console.log(mopidy.playback.next.description);
|
||||
|
||||
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
|
||||
by-name. Combinations of both, like we're used to from Python, isn't supported
|
||||
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
|
||||
passing parameters by-position.
|
||||
|
||||
Obviously, you'll want to get a return value from many of your method calls.
|
||||
Since everything is happening across the WebSocket and maybe even across the
|
||||
network, you'll get the results asynchronously. Instead of having to pass
|
||||
callbacks and errbacks to every method you call, the methods return "promise"
|
||||
objects, which you can use to pipe the future result as input to another
|
||||
method, or to hook up callback and errback functions.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var track = mopidy.playback.getCurrentTrack();
|
||||
// => ``track`` isn't a track, but a "promise" object
|
||||
|
||||
Instead, typical usage will look like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var printCurrentTrack = function (track) {
|
||||
if (track) {
|
||||
console.log("Currently playing:", track.name, "by",
|
||||
track.artists[0].name, "from", track.album.name);
|
||||
} else {
|
||||
console.log("No current track");
|
||||
}
|
||||
};
|
||||
|
||||
mopidy.playback.getCurrentTrack().then(
|
||||
printCurrentTrack, console.error.bind(console));
|
||||
|
||||
The first function passed to ``then()``, ``printCurrentTrack``, is the callback
|
||||
that will be called if the method call succeeds. The second function,
|
||||
``console.error``, is the errback that will be called if anything goes wrong.
|
||||
If you don't hook up an errback, debugging will be hard as errors will silently
|
||||
go missing.
|
||||
|
||||
For debugging, you may be interested in errors from function without
|
||||
interesting return values as well. In that case, you can pass ``null`` as the
|
||||
callback:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.playback.next().then(null, console.error.bind(console));
|
||||
|
||||
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||
implementation known as `when.js <https://github.com/cujojs/when>`_. Please
|
||||
refer to when.js' documentation or the standard for further details on how to
|
||||
work with promise objects.
|
||||
|
||||
|
||||
Cleaning up
|
||||
-----------
|
||||
|
||||
If you for some reason want to clean up after Mopidy.js before the web page is
|
||||
closed or navigated away from, you can close the WebSocket, unregister all
|
||||
event listeners, and delete the object like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
// Close the WebSocket without reconnecting. Letting the object be garbage
|
||||
// collected will have the same effect, so this isn't strictly necessary.
|
||||
mopidy.close();
|
||||
|
||||
// Unregister all event listeners. If you don't do this, you may have
|
||||
// lingering references to the object causing the garbage collector to not
|
||||
// clean up after it.
|
||||
mopidy.off();
|
||||
|
||||
// Delete your reference to the object, so it can be garbage collected.
|
||||
mopidy = null;
|
||||
|
||||
|
||||
Example to get started with
|
||||
---------------------------
|
||||
|
||||
1. Make sure that you've installed all dependencies required by
|
||||
:ref:`ext-http`.
|
||||
|
||||
2. Create an empty directory for your web client.
|
||||
|
||||
3. Change the :confval:`http/static_dir` config value to point to your new
|
||||
directory.
|
||||
|
||||
4. Start/restart Mopidy.
|
||||
|
||||
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
|
||||
world!".
|
||||
|
||||
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
|
||||
there.
|
||||
|
||||
7. Include Mopidy.js in your web page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
8. Add one of the following Mopidy.js examples of how to queue and start
|
||||
playback of your first playlist either to your web page or a JavaScript file
|
||||
that you include in your web page.
|
||||
|
||||
"Imperative" style:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var consoleError = console.error.bind(console);
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var queueAndPlayFirstPlaylist = function () {
|
||||
mopidy.playlists.getPlaylists().then(function (playlists) {
|
||||
var playlist = playlists[0];
|
||||
console.log("Loading playlist:", playlist.name);
|
||||
mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) {
|
||||
mopidy.playback.play(tlTracks[0]).then(function () {
|
||||
mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
Approximately the same behavior in a more functional style, using chaining
|
||||
of promisies.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var consoleError = console.error.bind(console);
|
||||
|
||||
var getFirst = function (list) {
|
||||
return list[0];
|
||||
};
|
||||
|
||||
var extractTracks = function (playlist) {
|
||||
return playlist.tracks;
|
||||
};
|
||||
|
||||
var printTypeAndName = function (model) {
|
||||
console.log(model.__model__ + ": " + model.name);
|
||||
// By returning the playlist, this function can be inserted
|
||||
// anywhere a model with a name is piped in the chain.
|
||||
return model;
|
||||
};
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var printNowPlaying = function () {
|
||||
// By returning any arguments we get, the function can be inserted
|
||||
// anywhere in the chain.
|
||||
var args = arguments;
|
||||
return mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
return args;
|
||||
});
|
||||
};
|
||||
|
||||
var queueAndPlayFirstPlaylist = function () {
|
||||
mopidy.playlists.getPlaylists()
|
||||
// => list of Playlists
|
||||
.then(getFirst, consoleError)
|
||||
// => Playlist
|
||||
.then(printTypeAndName, consoleError)
|
||||
// => Playlist
|
||||
.then(extractTracks, consoleError)
|
||||
// => list of Tracks
|
||||
.then(mopidy.tracklist.add, consoleError)
|
||||
// => list of TlTracks
|
||||
.then(getFirst, consoleError)
|
||||
// => TlTrack
|
||||
.then(mopidy.playback.play, consoleError)
|
||||
// => null
|
||||
.then(printNowPlaying, consoleError);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
9. The web page should now queue and play your first playlist every time your
|
||||
load it. See the browser's console for output from the function, any errors,
|
||||
and all events that are emitted.
|
||||
Event objects will always have a key named ``event`` whose value is the event
|
||||
type. Depending on the event type, the event may include additional fields for
|
||||
related data. The events maps directly to the :class:`mopidy.core.CoreListener`
|
||||
API. Refer to the :class:`~mopidy.core.CoreListener` method names is the
|
||||
available event types. The :class:`~mopidy.core.CoreListener` method's keyword
|
||||
arguments are all included as extra fields on the event objects. Example event
|
||||
message::
|
||||
|
||||
{"event": "track_playback_started", "track": {...}}
|
||||
|
||||
@ -21,9 +21,12 @@ API reference
|
||||
backends
|
||||
core
|
||||
audio
|
||||
mixer
|
||||
frontends
|
||||
commands
|
||||
ext
|
||||
config
|
||||
zeroconf
|
||||
http-server
|
||||
http
|
||||
js
|
||||
|
||||
440
docs/api/js.rst
Normal file
@ -0,0 +1,440 @@
|
||||
.. _mopidy-js:
|
||||
|
||||
****************************
|
||||
Mopidy.js JavaScript library
|
||||
****************************
|
||||
|
||||
We've made a JavaScript library, Mopidy.js, which wraps the
|
||||
:ref:`websocket-api` and gets you quickly started with working on your client
|
||||
instead of figuring out how to communicate with Mopidy.
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Since the Mopidy.js API exposes our internal core API directly it is to be
|
||||
regarded as **experimental**. We cannot promise to keep any form of
|
||||
backwards compatibility between releases as we will need to change the core
|
||||
API while working out how to support new use cases. Thus, if you use this
|
||||
API, you must expect to do small adjustments to your client for every
|
||||
release of Mopidy.
|
||||
|
||||
From Mopidy 1.0 and onwards, we intend to keep the core API far more
|
||||
stable.
|
||||
|
||||
|
||||
Getting the library for browser use
|
||||
===================================
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP extension is enabled, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
Thus, if you use Mopidy to host your web client, like described above, you can
|
||||
load the latest version of Mopidy.js by adding the following script tag to your
|
||||
HTML file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
If you don't use Mopidy to host your web client, you can find the JS files in
|
||||
the Git repo at:
|
||||
|
||||
- ``mopidy/http/data/mopidy.js``
|
||||
- ``mopidy/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Getting the library for Node.js or Browserify use
|
||||
=================================================
|
||||
|
||||
If you want to use Mopidy.js from Node.js or on the web through Browserify, you
|
||||
can install Mopidy.js using npm::
|
||||
|
||||
npm install mopidy
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Getting the library for development on the library
|
||||
==================================================
|
||||
|
||||
If you want to work on the Mopidy.js library itself, you'll find a complete
|
||||
development setup in the ``js/`` dir in our repo. The instructions in
|
||||
``js/README.md`` will guide you on your way.
|
||||
|
||||
|
||||
Creating an instance
|
||||
====================
|
||||
|
||||
Once you have Mopidy.js loaded, you need to create an instance of the wrapper:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy();
|
||||
|
||||
When you instantiate ``Mopidy()`` without arguments, it will connect to
|
||||
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
|
||||
your web client using Mopidy's web server, or if you use Mopidy.js from a
|
||||
Node.js environment, you'll need to pass the URL to the WebSocket end point:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy({
|
||||
webSocketUrl: "ws://localhost:6680/mopidy/ws/"
|
||||
});
|
||||
|
||||
It is also possible to create an instance first and connect to the WebSocket
|
||||
later:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var mopidy = new Mopidy({autoConnect: false});
|
||||
// ... do other stuff, like hooking up events ...
|
||||
mopidy.connect();
|
||||
|
||||
When creating an instance, you can specify the following settings:
|
||||
|
||||
``autoConnect``
|
||||
Whether or not to connect to the WebSocket on instance creation. Defaults
|
||||
to true.
|
||||
|
||||
``backoffDelayMin``
|
||||
The minimum number of milliseconds to wait after a connection error before
|
||||
we try to reconnect. For every failed attempt, the backoff delay is doubled
|
||||
until it reaches ``backoffDelayMax``. Defaults to 1000.
|
||||
|
||||
``backoffDelayMax``
|
||||
The maximum number of milliseconds to wait after a connection error before
|
||||
we try to reconnect. Defaults to 64000.
|
||||
|
||||
``callingConvention``
|
||||
Which calling convention to use when calling methods.
|
||||
|
||||
If set to "by-position-only", methods expect to be called with positional
|
||||
arguments, like ``mopidy.foo.bar(null, true, 2)``.
|
||||
|
||||
If set to "by-position-or-by-name", methods expect to be called either with
|
||||
an array of position arguments, like ``mopidy.foo.bar([null, true, 2])``,
|
||||
or with an object of named arguments, like ``mopidy.foo.bar({id: 2})``. The
|
||||
advantage of the "by-position-or-by-name" calling convention is that
|
||||
arguments with default values can be left out of the named argument object.
|
||||
Using named arguments also makes the code more readable, and more resistent
|
||||
to future API changes.
|
||||
|
||||
.. note::
|
||||
|
||||
For backwards compatibility, the default is "by-position-only". In the
|
||||
future, the default will change to "by-position-or-by-name". You should
|
||||
explicitly set this setting to your choice, so you won't be affected
|
||||
when the default changes.
|
||||
|
||||
.. versionadded:: 0.19 (Mopidy.js 0.4)
|
||||
|
||||
``console``
|
||||
If set, this object will be used to log errors from Mopidy.js. This is
|
||||
mostly useful for testing Mopidy.js.
|
||||
|
||||
``webSocket``
|
||||
An existing WebSocket object to be used instead of creating a new
|
||||
WebSocket. Defaults to undefined.
|
||||
|
||||
``webSocketUrl``
|
||||
URL used when creating new WebSocket objects. Defaults to
|
||||
``ws://<document.location.host>/mopidy/ws``, or
|
||||
``ws://localhost/mopidy/ws`` if ``document.location.host`` isn't
|
||||
available, like it is in the browser environment.
|
||||
|
||||
|
||||
Hooking up to events
|
||||
====================
|
||||
|
||||
Once you have a Mopidy.js object, you can hook up to the events it emits. To
|
||||
explore your possibilities, it can be useful to subscribe to all events and log
|
||||
them:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on(console.log.bind(console));
|
||||
|
||||
Several types of events are emitted:
|
||||
|
||||
- You can get notified about when the Mopidy.js object is connected to the
|
||||
server and ready for method calls, when it's offline, and when it's trying to
|
||||
reconnect to the server by looking at the events ``state:online``,
|
||||
``state:offline``, ``reconnectionPending``, and ``reconnecting``.
|
||||
|
||||
- You can get events sent from the Mopidy server by looking at the events with
|
||||
the name prefix ``event:``, like ``event:trackPlaybackStarted``.
|
||||
|
||||
- You can introspect what happens internally on the WebSocket by looking at the
|
||||
events emitted with the name prefix ``websocket:``.
|
||||
|
||||
Mopidy.js uses the event emitter library `BANE
|
||||
<https://github.com/busterjs/bane>`_, so you should refer to BANE's
|
||||
short API documentation to see how you can hook up your listeners to the
|
||||
different events.
|
||||
|
||||
|
||||
Calling core API methods
|
||||
========================
|
||||
|
||||
Once your Mopidy.js object has connected to the Mopidy server and emits the
|
||||
``state:online`` event, it is ready to accept core API method calls:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on("state:online", function () {
|
||||
mopidy.playback.next();
|
||||
});
|
||||
|
||||
Any calls you make before the ``state:online`` event is emitted will fail. If
|
||||
you've hooked up an errback (more on that a bit later) to the promise returned
|
||||
from the call, the errback will be called with a ``Mopidy.ConnectionError``
|
||||
instance.
|
||||
|
||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
|
||||
API attributes is *not* available, but that shouldn't be a problem as we've
|
||||
added (undocumented) getters and setters for all of them, so you can access the
|
||||
attributes as well from JavaScript. For example, the
|
||||
:attr:`mopidy.core.PlaybackController.state` attribute is available in
|
||||
JSON-RPC as the method ``core.playback.get_state`` and in Mopidy.js as
|
||||
``mopidy.playback.getState()``.
|
||||
|
||||
Both the WebSocket API and the JavaScript API are based on introspection of the
|
||||
core Python API. Thus, they will always be up to date and immediately reflect
|
||||
any changes we do to the core API.
|
||||
|
||||
The best way to explore the JavaScript API, is probably by opening your
|
||||
browser's console, and using its tab completion to navigate the API. You'll
|
||||
find the Mopidy core API exposed under ``mopidy.playback``,
|
||||
``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``.
|
||||
|
||||
All methods in the JavaScript API have an associated data structure describing
|
||||
the Python params it expects, and most methods also have the Python API
|
||||
documentation available. This is available right there in the browser console,
|
||||
by looking at the method's ``description`` and ``params`` attributes:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
console.log(mopidy.playback.next.params);
|
||||
console.log(mopidy.playback.next.description);
|
||||
|
||||
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
|
||||
by-name. Combinations of both, like we're used to from Python, isn't supported
|
||||
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
|
||||
passing parameters by-position.
|
||||
|
||||
Obviously, you'll want to get a return value from many of your method calls.
|
||||
Since everything is happening across the WebSocket and maybe even across the
|
||||
network, you'll get the results asynchronously. Instead of having to pass
|
||||
callbacks and errbacks to every method you call, the methods return "promise"
|
||||
objects, which you can use to pipe the future result as input to another
|
||||
method, or to hook up callback and errback functions.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var track = mopidy.playback.getCurrentTrack();
|
||||
// => ``track`` isn't a track, but a "promise" object
|
||||
|
||||
Instead, typical usage will look like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var printCurrentTrack = function (track) {
|
||||
if (track) {
|
||||
console.log("Currently playing:", track.name, "by",
|
||||
track.artists[0].name, "from", track.album.name);
|
||||
} else {
|
||||
console.log("No current track");
|
||||
}
|
||||
};
|
||||
|
||||
mopidy.playback.getCurrentTrack()
|
||||
.done(printCurrentTrack);
|
||||
|
||||
The function passed to ``done()``, ``printCurrentTrack``, is the callback
|
||||
that will be called if the method call succeeds. If anything goes wrong,
|
||||
``done()`` will throw an exception.
|
||||
|
||||
If you want to explicitly handle any errors and avoid an exception being
|
||||
thrown, you can register an error handler function anywhere in a promise
|
||||
chain. The function will be called with the error object as the only argument:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.playback.getCurrentTrack()
|
||||
.catch(console.error.bind(console));
|
||||
.done(printCurrentTrack);
|
||||
|
||||
You can also register the error handler at the end of the promise chain by
|
||||
passing it as the second argument to ``done()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.playback.getCurrentTrack()
|
||||
.done(printCurrentTrack, console.error.bind(console));
|
||||
|
||||
If you don't hook up an error handler function and never call ``done()`` on the
|
||||
promise object, when.js will log warnings to the console that you have
|
||||
unhandled errors. In general, unhandled errors will not go silently missing.
|
||||
|
||||
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||
implementation known as `when.js <https://github.com/cujojs/when>`_. Please
|
||||
refer to when.js' documentation or the standard for further details on how to
|
||||
work with promise objects.
|
||||
|
||||
|
||||
Cleaning up
|
||||
===========
|
||||
|
||||
If you for some reason want to clean up after Mopidy.js before the web page is
|
||||
closed or navigated away from, you can close the WebSocket, unregister all
|
||||
event listeners, and delete the object like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
// Close the WebSocket without reconnecting. Letting the object be garbage
|
||||
// collected will have the same effect, so this isn't strictly necessary.
|
||||
mopidy.close();
|
||||
|
||||
// Unregister all event listeners. If you don't do this, you may have
|
||||
// lingering references to the object causing the garbage collector to not
|
||||
// clean up after it.
|
||||
mopidy.off();
|
||||
|
||||
// Delete your reference to the object, so it can be garbage collected.
|
||||
mopidy = null;
|
||||
|
||||
|
||||
Example to get started with
|
||||
===========================
|
||||
|
||||
1. Make sure that you've installed all dependencies required by
|
||||
:ref:`ext-http`.
|
||||
|
||||
2. Create an empty directory for your web client.
|
||||
|
||||
3. Change the :confval:`http/static_dir` config value to point to your new
|
||||
directory.
|
||||
|
||||
4. Start/restart Mopidy.
|
||||
|
||||
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
|
||||
world!".
|
||||
|
||||
6. Visit http://localhost:6680/ to confirm that you can view your new HTML file
|
||||
there.
|
||||
|
||||
7. Include Mopidy.js in your web page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script type="text/javascript" src="/mopidy/mopidy.min.js"></script>
|
||||
|
||||
8. Add one of the following Mopidy.js examples of how to queue and start
|
||||
playback of your first playlist either to your web page or a JavaScript file
|
||||
that you include in your web page.
|
||||
|
||||
"Imperative" style:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var queueAndPlay = function (playlistNum, trackNum) {
|
||||
playlistNum = playlistNum || 0;
|
||||
trackNum = trackNum || 0;
|
||||
mopidy.playlists.getPlaylists().then(function (playlists) {
|
||||
var playlist = playlists[playlistNum];
|
||||
console.log("Loading playlist:", playlist.name);
|
||||
return mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) {
|
||||
return mopidy.playback.play(tlTracks[trackNum]).then(function () {
|
||||
return mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(console.error.bind(console)) // Handle errors here
|
||||
.done(); // ...or they'll be thrown here
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlay);
|
||||
|
||||
Approximately the same behavior in a more functional style, using chaining
|
||||
of promises.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var get = function (key, object) {
|
||||
return object[key];
|
||||
};
|
||||
|
||||
var printTypeAndName = function (model) {
|
||||
console.log(model.__model__ + ": " + model.name);
|
||||
// By returning the playlist, this function can be inserted
|
||||
// anywhere a model with a name is piped in the chain.
|
||||
return model;
|
||||
};
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
};
|
||||
|
||||
var printNowPlaying = function () {
|
||||
// By returning any arguments we get, the function can be inserted
|
||||
// anywhere in the chain.
|
||||
var args = arguments;
|
||||
return mopidy.playback.getCurrentTrack()
|
||||
.then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
return args;
|
||||
});
|
||||
};
|
||||
|
||||
var queueAndPlay = function (playlistNum, trackNum) {
|
||||
playlistNum = playlistNum || 0;
|
||||
trackNum = trackNum || 0;
|
||||
mopidy.playlists.getPlaylists()
|
||||
// => list of Playlists
|
||||
.fold(get, playlistNum)
|
||||
// => Playlist
|
||||
.then(printTypeAndName)
|
||||
// => Playlist
|
||||
.fold(get, 'tracks')
|
||||
// => list of Tracks
|
||||
.then(mopidy.tracklist.add)
|
||||
// => list of TlTracks
|
||||
.fold(get, trackNum)
|
||||
// => TlTrack
|
||||
.then(mopidy.playback.play)
|
||||
// => null
|
||||
.then(printNowPlaying)
|
||||
// => null
|
||||
.catch(console.error.bind(console)) // Handle errors here
|
||||
// => null
|
||||
.done(); // ...or they'll be thrown here
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlay);
|
||||
|
||||
9. The web page should now queue and play your first playlist every time you
|
||||
load it. See the browser's console for output from the function, any errors,
|
||||
and all events that are emitted.
|
||||
20
docs/api/mixer.rst
Normal file
@ -0,0 +1,20 @@
|
||||
.. _mixer-api:
|
||||
|
||||
***************
|
||||
Audio mixer API
|
||||
***************
|
||||
|
||||
.. module:: mopidy.mixer
|
||||
:synopsis: The audio mixer API
|
||||
|
||||
.. autoclass:: mopidy.mixer.Mixer
|
||||
:members:
|
||||
|
||||
.. autoclass:: mopidy.mixer.MixerListener
|
||||
:members:
|
||||
|
||||
|
||||
Mixer implementations
|
||||
=====================
|
||||
|
||||
See :ref:`ext-mixers`.
|
||||
@ -5,6 +5,225 @@ Changelog
|
||||
This changelog is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
v0.19.0 (2014-07-21)
|
||||
====================
|
||||
|
||||
The focus of 0.19 have been on improving the MPD implementation, replacing
|
||||
GStreamer mixers with our own mixer API, and on making web clients installable
|
||||
with ``pip``, like any other Mopidy extension.
|
||||
|
||||
Since the release of 0.18, we've closed or merged 53 issues and pull requests
|
||||
through about 445 commits by :ref:`12 people <authors>`, including five new
|
||||
guys. Thanks to everyone that has contributed!
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- Mopidy now requires Tornado >= 3.1.
|
||||
|
||||
- Mopidy no longer requires CherryPy or ws4py. Previously, these were optional
|
||||
dependencies required for the HTTP frontend to work.
|
||||
|
||||
**Backend API**
|
||||
|
||||
- *Breaking change:* Imports of the backend API from
|
||||
:mod:`mopidy.backends` no longer works. The new API introuced in v0.18 is now
|
||||
required. Most extensions already use the new API location.
|
||||
|
||||
**Commands**
|
||||
|
||||
- The ``mopidy-convert-config`` tool for migrating the ``setings.py``
|
||||
configuration file used by Mopidy up until 0.14 to the new config file format
|
||||
has been removed after over a year of trusty service. If you still need to
|
||||
convert your old ``settings.py`` configuration file, do so using and older
|
||||
release, like Mopidy 0.18, or migrate the configuration to the new format by
|
||||
hand.
|
||||
|
||||
**Configuration**
|
||||
|
||||
- Add ``optional=True`` support to :class:`mopidy.config.Boolean`.
|
||||
|
||||
**Logging**
|
||||
|
||||
- Fix proper decoding of exception messages that depends on the user's locale.
|
||||
|
||||
- Colorize logs depending on log level. This can be turned off with the new
|
||||
:confval:`logging/color` configuration. (Fixes: :issue:`772`)
|
||||
|
||||
**Extension support**
|
||||
|
||||
- *Breaking change:* Removed the :class:`~mopidy.ext.Extension` methods that
|
||||
were deprecated in 0.18: :meth:`~mopidy.ext.Extension.get_backend_classes`,
|
||||
:meth:`~mopidy.ext.Extension.get_frontend_classes`, and
|
||||
:meth:`~mopidy.ext.Extension.register_gstreamer_elements`. Use
|
||||
:meth:`mopidy.ext.Extension.setup` instead, as most extensions already do.
|
||||
|
||||
**Audio**
|
||||
|
||||
- *Breaking change:* Removed support for GStreamer mixers. GStreamer 1.x does
|
||||
not support volume control, so we changed to use software mixing by default
|
||||
in v0.17.0. Now, we're removing support for all other GStreamer mixers and
|
||||
are reintroducing mixers as something extensions can provide independently of
|
||||
GStreamer. (Fixes: :issue:`665`, PR: :issue:`760`)
|
||||
|
||||
- *Breaking change:* Changed the :confval:`audio/mixer` config value to refer
|
||||
to Mopidy mixer extensions instead of GStreamer mixers. The default value,
|
||||
``software``, still has the same behavior. All other values will either no
|
||||
longer work or will at the very least require you to install an additional
|
||||
extension.
|
||||
|
||||
- Changed the :confval:`audio/mixer_volume` config value behavior from
|
||||
affecting GStreamer mixers to affecting Mopidy mixer extensions instead. The
|
||||
end result should be the same without any changes to this config value.
|
||||
|
||||
- Deprecated the :confval:`audio/mixer_track` config value. This config value
|
||||
is no longer in use. Mixer extensions that need additional configuration
|
||||
handle this themselves.
|
||||
|
||||
- Use :ref:`proxy-config` when streaming media from the Internet. (Partly
|
||||
fixing :issue:`390`)
|
||||
|
||||
- Fix proper decoding of exception messages that depends on the user's locale.
|
||||
|
||||
- Fix recognition of ASX and XSPF playlists with tags in all caps or with
|
||||
carriage return line endings. (Fixes: :issue:`687`)
|
||||
|
||||
- Support simpler ASX playlist variant with ``<ENTRY>`` elements without
|
||||
children.
|
||||
|
||||
- Added ``target_state`` attribute to the audio layer's
|
||||
:meth:`~mopidy.audio.AudioListener.state_changed` event. Currently, it is
|
||||
:class:`None` except when we're paused because of buffering. Then the new
|
||||
field exposes our target state after buffering has completed.
|
||||
|
||||
**Mixers**
|
||||
|
||||
- Added new :class:`mopidy.mixer.Mixer` API which can be implemented by
|
||||
extensions.
|
||||
|
||||
- Created a bundled extension, :ref:`ext-softwaremixer`, for controlling volume
|
||||
in software in GStreamer's pipeline. This is Mopidy's default mixer. To use
|
||||
this mixer, set the :confval:`audio/mixer` config value to ``software``.
|
||||
|
||||
- Created an external extension, `Mopidy-ALSAMixer
|
||||
<https://github.com/mopidy/mopidy-alsamixer/>`_, for controlling volume with
|
||||
hardware through ALSA. To use this mixer, install the extension, and set the
|
||||
:confval:`audio/mixer` config value to ``alsamixer``.
|
||||
|
||||
**HTTP frontend**
|
||||
|
||||
- CherryPy and ws4py have been replaced with Tornado. This will hopefully
|
||||
reduce CPU usage on OS X (:issue:`445`) and improve error handling in corner
|
||||
cases, like when returning from suspend (:issue:`718`).
|
||||
|
||||
- Added support for packaging web clients as Mopidy extensions and installing
|
||||
them using pip. See the :ref:`http-server-api` for details. (Fixes:
|
||||
:issue:`440`)
|
||||
|
||||
- Added web page at ``/mopidy/`` which lists all web clients installed as
|
||||
Mopidy extensions. (Fixes: :issue:`440`)
|
||||
|
||||
- Added support for extending the HTTP frontend with additional server side
|
||||
functionality. See :ref:`http-server-api` for details.
|
||||
|
||||
- Exposed the core API using HTTP POST requests with JSON-RPC payloads at
|
||||
``/mopidy/rpc``. This is the same JSON-RPC interface as is exposed over the
|
||||
WebSocket at ``/mopidy/ws``, so you can run any core API command.
|
||||
|
||||
The HTTP POST interfaces does not give you access to events from Mopidy, like
|
||||
the WebSocket does. The WebSocket interface is still recommended for web
|
||||
clients. The HTTP POST interface may be easier to use for simpler programs,
|
||||
that just needs to query the currently playing track or similar. See
|
||||
:ref:`http-post-api` for details.
|
||||
|
||||
- If Zeroconf is enabled, we now announce the ``_mopidy-http._tcp`` service in
|
||||
addition to ``_http._tcp``. This is to make it easier to automatically find
|
||||
Mopidy's HTTP server among other Zeroconf-published HTTP servers on the
|
||||
local network.
|
||||
|
||||
**Mopidy.js client library**
|
||||
|
||||
This version has been released to npm as Mopidy.js v0.4.0.
|
||||
|
||||
- Update Mopidy.js to use when.js 3. If you maintain a Mopidy client, you
|
||||
should review the `differences between when.js 2 and 3
|
||||
<https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x>`_
|
||||
and the `when.js debugging guide
|
||||
<https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises>`_.
|
||||
|
||||
- All of Mopidy.js' promise rejection values are now of the Error type. This
|
||||
ensures that all JavaScript VMs will show a useful stack trace if a rejected
|
||||
promise's value is used to throw an exception. To allow catch clauses to
|
||||
handle different errors differently, server side errors are of the type
|
||||
``Mopidy.ServerError``, and connection related errors are of the type
|
||||
``Mopidy.ConnectionError``.
|
||||
|
||||
- Add support for method calls with by-name arguments. The old calling
|
||||
convention, ``by-position-only``, is still the default, but this will
|
||||
change in the future. A warning is logged to the console if you don't
|
||||
explicitly select a calling convention. See the :ref:`mopidy-js` docs for
|
||||
details.
|
||||
|
||||
**MPD frontend**
|
||||
|
||||
- Proper command tokenization for MPD requests. This replaces the old regex
|
||||
based system with an MPD protocol specific tokenizer responsible for breaking
|
||||
requests into pieces before the handlers have at them.
|
||||
(Fixes: :issue:`591` and :issue:`592`)
|
||||
|
||||
- Updated command handler system. As part of the tokenizer cleanup we've
|
||||
updated how commands are registered and making it simpler to create new
|
||||
handlers.
|
||||
|
||||
- Simplified a bunch of handlers. All the "browse" type commands now use a
|
||||
common browse helper under the hood for less repetition. Likewise the query
|
||||
handling of "search" commands has been somewhat simplified.
|
||||
|
||||
- Adds placeholders for missing MPD commands, preparing the way for bumping the
|
||||
protocol version once they have been added.
|
||||
|
||||
- Respond to all pending requests before closing connection. (PR: :issue:`722`)
|
||||
|
||||
- Stop incorrectly catching `LookupError` in command handling.
|
||||
(Fixes: :issue:`741`)
|
||||
|
||||
- Browse support for playlists and albums has been added. (PR: :issue:`749`,
|
||||
:issue:`754`)
|
||||
|
||||
- The ``lsinfo`` command now returns browse results before local playlists.
|
||||
This is helpful as not all clients sort the returned items. (PR:
|
||||
:issue:`755`)
|
||||
|
||||
- Browse now supports different entries with identical names. (PR:
|
||||
:issue:`762`)
|
||||
|
||||
- Search terms that are empty or consists of only whitespace are no longer
|
||||
included in the search query sent to backends. (PR: :issue:`758`)
|
||||
|
||||
**Local backend**
|
||||
|
||||
- The JSON local library backend now logs a friendly message telling you about
|
||||
``mopidy local scan`` if you don't have a local library cache. (Fixes:
|
||||
:issue:`711`)
|
||||
|
||||
- The ``local scan`` command now use multiple threads to walk the file system
|
||||
and check files' modification time. This speeds up scanning, escpecially
|
||||
when scanning remote file systems over e.g. NFS.
|
||||
|
||||
- the ``local scan`` command now creates necessary folders if they don't
|
||||
already exist. Previously, this was only done by the Mopidy server, so doing
|
||||
a ``local scan`` before running the server the first time resulted in a
|
||||
crash. (Fixes: :issue:`703`)
|
||||
|
||||
- Fix proper decoding of exception messages that depends on the user's locale.
|
||||
|
||||
**Stream backend**
|
||||
|
||||
- Add config value :confval:`stream/metadata_blacklist` to blacklist certain
|
||||
URIs we should not open to read metadata from before they are opened for
|
||||
playback. This is typically needed for services that invalidate URIs after a
|
||||
single use. (Fixes: :issue:`660`)
|
||||
|
||||
|
||||
v0.18.3 (2014-02-16)
|
||||
====================
|
||||
|
||||
@ -649,7 +868,7 @@ one new.
|
||||
To ease migration we've made a tool named :option:`mopidy-convert-config` for
|
||||
automatically converting the old ``settings.py`` to a new ``mopidy.conf``
|
||||
file. This tool takes care of all the renamed config values as well. See
|
||||
:ref:`mopidy-convert-config` for details on how to use it.
|
||||
``mopidy-convert-config`` for details on how to use it.
|
||||
|
||||
- A long wanted feature: You can now enable or disable specific frontends or
|
||||
backends without having to redefine :attr:`~mopidy.settings.FRONTENDS` or
|
||||
|
||||
@ -15,52 +15,33 @@ created one, please notify us so we can include your client on this page.
|
||||
See :ref:`http-api` for details on how to build your own web client.
|
||||
|
||||
|
||||
woutervanwijk/Mopidy-Webclient
|
||||
==============================
|
||||
Mopidy MusicBox Webclient
|
||||
=========================
|
||||
|
||||
.. image:: woutervanwijk-mopidy-webclient.png
|
||||
.. image:: mopidy-musicbox-webclient.png
|
||||
:width: 1275
|
||||
:height: 600
|
||||
|
||||
The first web client for Mopidy, made with jQuery Mobile by Wouter van Wijk.
|
||||
Also the web client used for Wouter's popular `Pi Musicbox
|
||||
<http://www.woutervanwijk.nl/pimusicbox/>`_ image for Raspberry Pi.
|
||||
<http://www.pimusicbox.com/>`_ image for Raspberry Pi.
|
||||
|
||||
With Mopidy Browser Client, you can play your music on your computer (or
|
||||
Rapsberry Pi) and remotely control it from a computer, phone, tablet,
|
||||
With Mopidy MusicBox Webclient, you can play your music on your computer
|
||||
(Raspberry Pi) and remotely control it from a computer, phone, tablet,
|
||||
laptop. From your couch.
|
||||
|
||||
-- https://github.com/woutervanwijk/Mopidy-WebClient
|
||||
This is a responsive HTML/JS/CSS client especially written for Mopidy, a
|
||||
music server. Responsive, so it works on desktop and mobile browsers. You
|
||||
can browse, search and play albums, artists, playlists, and it has cover
|
||||
art from Last.fm.
|
||||
|
||||
-- https://github.com/woutervanwijk/Mopidy-MusicBox-Webclient
|
||||
|
||||
|
||||
Mopidy Lux
|
||||
==========
|
||||
|
||||
.. image:: dz0ny-mopidy-lux.png
|
||||
:width: 1275
|
||||
:height: 795
|
||||
|
||||
A Mopidy web client made with AngularJS by Janez Troha.
|
||||
|
||||
A shiny new remote web control interface for Mopidy player.
|
||||
|
||||
-- https://github.com/dz0ny/mopidy-lux
|
||||
.. include:: /ext/lux.rst
|
||||
|
||||
|
||||
Moped
|
||||
=====
|
||||
|
||||
.. image:: martijnboland-moped.png
|
||||
:width: 720
|
||||
:height: 450
|
||||
|
||||
A Mopidy web client made with Durandal and KnockoutJS by Martijn Boland.
|
||||
|
||||
Moped is a responsive web client for the Mopidy music server. It is
|
||||
inspired by Mopidy-Webclient, but built from scratch based on a different
|
||||
technology stack with Durandal and Bootstrap 3.
|
||||
|
||||
-- https://github.com/martijnboland/moped
|
||||
.. include:: /ext/moped.rst
|
||||
|
||||
|
||||
JukePi
|
||||
@ -102,6 +83,23 @@ A Mopidy web client made by Argonaut in SF for their office jukebox.
|
||||
-- http://blog.argonautinc.com/post/83027259908/music-is-pretty-important-to-our-culture-and
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
Mopify
|
||||
======
|
||||
|
||||
An in-development web client that clones the Spotify user interface on top of
|
||||
Mopidy and the Spotify web APIs.
|
||||
|
||||
A Mopidy web client based on the Spotify webbased interface. If you use
|
||||
Mopidy in combination with local music this client probably won't work.
|
||||
This client uses the Spotify and EchoNest API to speed up searching and
|
||||
artist/album lookup.
|
||||
|
||||
-- https://github.com/dirkgroenen/Mopify
|
||||
|
||||
|
||||
>>>>>>> develop
|
||||
Other web clients
|
||||
=================
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@ -60,6 +60,14 @@ supported)" mode because the client tries to fetch all known metadata and do
|
||||
the search on the client side. The two other search modes works nicely, so this
|
||||
is not a problem.
|
||||
|
||||
The library view is very slow when used together with Mopidy-Spotify. A
|
||||
workaround is to edit the ncmpcpp configuration file
|
||||
(:file:`~/.ncmpcpp/config`) and set::
|
||||
|
||||
media_library_display_date = "no"
|
||||
|
||||
With this change ncmpcpp's library view will still be a bit slow, but usable.
|
||||
|
||||
|
||||
ncmpc
|
||||
-----
|
||||
|
||||
@ -83,11 +83,11 @@ Additionally, extensions can provide extra commands. Run `mopidy --help`
|
||||
for a list of what is available on your system and command-specific help.
|
||||
Commands for disabled extensions will be listed, but can not be run.
|
||||
|
||||
.. cmdoption:: local clear
|
||||
.. describe:: local clear
|
||||
|
||||
Clear local media files from the local library.
|
||||
|
||||
.. cmdoption:: local scan
|
||||
.. describe:: local scan
|
||||
|
||||
Scan local media files present in your library.
|
||||
|
||||
@ -130,12 +130,6 @@ The ``mopidy config`` output shows the effect of the :option:`--option` flags::
|
||||
mopidy -o mpd/enabled=false -o spotify/bitrate=320 config
|
||||
|
||||
|
||||
See also
|
||||
========
|
||||
|
||||
:ref:`mopidy-convert-config(1) <mopidy-convert-config>`
|
||||
|
||||
|
||||
Reporting bugs
|
||||
==============
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
.. _commands:
|
||||
|
||||
********
|
||||
Commands
|
||||
********
|
||||
|
||||
Mopidy comes with the following commands:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
**
|
||||
@ -1,98 +0,0 @@
|
||||
.. _mopidy-convert-config:
|
||||
|
||||
*****************************
|
||||
mopidy-convert-config command
|
||||
*****************************
|
||||
|
||||
Synopsis
|
||||
========
|
||||
|
||||
mopidy-convert-config
|
||||
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
Mopidy is a music server which can play music both from multiple sources, like
|
||||
your local hard drive, radio streams, and from Spotify and SoundCloud. Searches
|
||||
combines results from all music sources, and you can mix tracks from all
|
||||
sources in your play queue. Your playlists from Spotify or SoundCloud are also
|
||||
available for use.
|
||||
|
||||
The ``mopidy-convert-config`` command is used to convert :file:`settings.py`
|
||||
configuration files used by ``mopidy`` < 0.14 to the :file:`mopidy.conf` config
|
||||
file used by ``mopidy`` >= 0.14.
|
||||
|
||||
|
||||
Options
|
||||
=======
|
||||
|
||||
.. program:: mopidy-convert-config
|
||||
|
||||
This program does not take any options. It looks for the pre-0.14 settings file
|
||||
at :file:`{$XDG_CONFIG_DIR}/mopidy/settings.py`, and if it exists it converts
|
||||
it and ouputs a Mopidy 0.14 compatible ini-format configuration. If you don't
|
||||
already have a config file at :file:`{$XDG_CONFIG_DIR}/mopidy/mopidy.conf``,
|
||||
you're asked if you want to save the converted config to that file.
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Given the following contents in :file:`~/.config/mopidy/settings.py`:
|
||||
|
||||
::
|
||||
|
||||
LOCAL_MUSIC_PATH = u'~/music'
|
||||
MPD_SERVER_HOSTNAME = u'::'
|
||||
SPOTIFY_PASSWORD = u'secret'
|
||||
SPOTIFY_USERNAME = u'alice'
|
||||
|
||||
Running ``mopidy-convert-config`` will convert the config and create a new
|
||||
:file:`mopidy.conf` config file:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
$ mopidy-convert-config
|
||||
Checking /home/alice/.config/mopidy/settings.py
|
||||
Converted config:
|
||||
|
||||
[spotify]
|
||||
username = alice
|
||||
password = ********
|
||||
|
||||
[mpd]
|
||||
hostname = ::
|
||||
|
||||
[local]
|
||||
media_dir = ~/music
|
||||
|
||||
Write new config to /home/alice/.config/mopidy/mopidy.conf? [yN] y
|
||||
Done.
|
||||
|
||||
Contents of :file:`~/.config/mopidy/mopidy.conf` after the conversion:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[spotify]
|
||||
username = alice
|
||||
password = secret
|
||||
|
||||
[mpd]
|
||||
hostname = ::
|
||||
|
||||
[local]
|
||||
media_dir = ~/music
|
||||
|
||||
|
||||
See also
|
||||
========
|
||||
|
||||
:ref:`mopidy(1) <mopidy-cmd>`
|
||||
|
||||
|
||||
Reporting bugs
|
||||
==============
|
||||
|
||||
Report bugs to Mopidy's issue tracker at
|
||||
<https://github.com/mopidy/mopidy/issues>
|
||||
21
docs/conf.py
@ -35,8 +35,6 @@ class Mock(object):
|
||||
# glib.get_user_config_dir()
|
||||
return str
|
||||
elif (name[0] == name[0].upper()
|
||||
# gst.interfaces.MIXER_TRACK_*
|
||||
and not name.startswith('MIXER_TRACK_')
|
||||
# gst.PadTemplate
|
||||
and not name.startswith('PadTemplate')
|
||||
# dbus.String()
|
||||
@ -89,6 +87,7 @@ extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.extlinks',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
@ -141,19 +140,12 @@ latex_documents = [
|
||||
|
||||
man_pages = [
|
||||
(
|
||||
'commands/mopidy',
|
||||
'command',
|
||||
'mopidy',
|
||||
'music server',
|
||||
'',
|
||||
'1'
|
||||
),
|
||||
(
|
||||
'commands/mopidy-convert-config',
|
||||
'mopidy-convert-config',
|
||||
'migrate config files from mopidy pre-0.14',
|
||||
'',
|
||||
'1'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@ -165,3 +157,12 @@ extlinks = {
|
||||
'mpris': (
|
||||
'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'),
|
||||
}
|
||||
|
||||
|
||||
# -- Options for intersphinx extension ----------------------------------------
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/2', None),
|
||||
'pykka': ('http://www.pykka.org/en/latest/', None),
|
||||
'tornado': ('http://www.tornadoweb.org/en/stable/', None),
|
||||
}
|
||||
|
||||
@ -45,14 +45,6 @@ below, together with their default values. In addition, all :ref:`extensions
|
||||
defaults are documented on the :ref:`extension pages <ext>`.
|
||||
|
||||
|
||||
Migrating from pre 0.14
|
||||
=======================
|
||||
|
||||
For those users upgrading from versions prior to 0.14 we made
|
||||
the :option:`mopidy-convert-config` tool, to ease the process of migrating
|
||||
settings to the new config format.
|
||||
|
||||
|
||||
Default core configuration
|
||||
==========================
|
||||
|
||||
@ -72,23 +64,15 @@ Audio configuration
|
||||
|
||||
Audio mixer to use.
|
||||
|
||||
Expects a GStreamer mixer to use, typical values are: ``software``,
|
||||
``autoaudiomixer``, ``alsamixer``, ``pulsemixer``, ``ossmixer``, and
|
||||
``oss4mixer``.
|
||||
|
||||
The default is ``software``, which does volume control inside Mopidy before
|
||||
the audio is sent to the audio output. This mixer does not affect the
|
||||
volume of any other audio playback on the system. It is the only mixer that
|
||||
will affect the audio volume if you're streaming the audio from Mopidy
|
||||
through Shoutcast.
|
||||
|
||||
If you want to use a hardware mixer, try ``autoaudiomixer``. It attempts to
|
||||
select a sane hardware mixer for you automatically. When Mopidy is started,
|
||||
it will log what mixer ``autoaudiomixer`` selected, for example::
|
||||
|
||||
INFO Audio mixer set to "alsamixer" using track "Master"
|
||||
|
||||
Setting the config value to blank turns off volume control.
|
||||
If you want to use a hardware mixer, you need to install a Mopidy extension
|
||||
which integrates with your sound subsystem. E.g. for ALSA, install
|
||||
`Mopidy-ALSAMixer <https://github.com/mopidy/mopidy-alsamixer>`_.
|
||||
|
||||
.. confval:: audio/mixer_volume
|
||||
|
||||
@ -99,14 +83,6 @@ Audio configuration
|
||||
Setting the config value to blank leaves the audio mixer volume unchanged.
|
||||
For the software mixer blank means 100.
|
||||
|
||||
.. confval:: audio/mixer_track
|
||||
|
||||
Audio mixer track to use.
|
||||
|
||||
Name of the mixer track to use. If this is not set we will try to find the
|
||||
master output track. As an example, using ``alsamixer`` you would typically
|
||||
set this to ``Master`` or ``PCM``.
|
||||
|
||||
.. confval:: audio/output
|
||||
|
||||
Audio output to use.
|
||||
@ -131,6 +107,11 @@ Audio configuration
|
||||
Logging configuration
|
||||
---------------------
|
||||
|
||||
.. confval:: logging/color
|
||||
|
||||
Whether or not to colorize the console log based on log level. Defaults to
|
||||
``true``.
|
||||
|
||||
.. confval:: logging/console_format
|
||||
|
||||
The log format used for informational logging.
|
||||
@ -164,6 +145,8 @@ Logging configuration
|
||||
.. _the Python logging docs: http://docs.python.org/2/library/logging.config.html
|
||||
|
||||
|
||||
.. _proxy-config:
|
||||
|
||||
Proxy configuration
|
||||
-------------------
|
||||
|
||||
@ -278,13 +261,21 @@ server simultaneously. To use the SHOUTcast output, do the following:
|
||||
|
||||
#. You might also need to change the ``shout2send`` default settings, run
|
||||
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
|
||||
you want to change ``ip``, ``username``, ``password``, and ``mount``. For
|
||||
example:
|
||||
you want to change ``ip``, ``username``, ``password``, and ``mount``.
|
||||
|
||||
Example for MP3 streaming:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[audio]
|
||||
output = lame ! shout2send username="alice" password="secret" mount="mopidy"
|
||||
output = lame ! shout2send mount=mopidy ip=127.0.0.1 port=8000 password=hackme
|
||||
|
||||
Example for Ogg Vorbis streaming:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[audio]
|
||||
output = audioresample ! audioconvert ! vorbisenc ! oggmux ! shout2send mount=mopidy ip=127.0.0.1 port=8000 password=hackme
|
||||
|
||||
Other advanced setups are also possible for outputs. Basically, anything you
|
||||
can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
|
||||
@ -35,6 +35,10 @@ Making changes
|
||||
|
||||
#. Install dependencies as described in the :ref:`installation` section.
|
||||
|
||||
#. Install additional development dependencies::
|
||||
|
||||
pip install -r dev-requirements.txt
|
||||
|
||||
#. Checkout a new branch (usually based on ``develop``) and name it accordingly
|
||||
to what you intend to do.
|
||||
|
||||
@ -82,29 +86,33 @@ Testing
|
||||
Mopidy has quite good test coverage, and we would like all new code going into
|
||||
Mopidy to come with tests.
|
||||
|
||||
#. To run tests, you need a couple of dependencies. They can be installed using
|
||||
``pip``::
|
||||
|
||||
pip install --upgrade coverage flake8 mock nose
|
||||
|
||||
#. Then, to run all tests, go to the project directory and run::
|
||||
#. To run all tests, go to the project directory and run::
|
||||
|
||||
nosetests
|
||||
|
||||
To run tests with test coverage statistics, remember to specify the tests
|
||||
dir::
|
||||
To run tests with test coverage statistics::
|
||||
|
||||
nosetests --with-coverage tests/
|
||||
nosetests --with-coverage
|
||||
|
||||
Test coverage statistics can also be viewed online at
|
||||
`coveralls.io <https://coveralls.io/r/mopidy/mopidy>`_.
|
||||
|
||||
#. Check the code for errors and style issues using flake8::
|
||||
#. Always check the code for errors and style issues using flake8::
|
||||
|
||||
flake8 .
|
||||
flake8
|
||||
|
||||
For more documentation on testing, check out the `nose documentation
|
||||
<http://nose.readthedocs.org/>`_.
|
||||
If successful, the command will not print anything at all.
|
||||
|
||||
#. Finally, there is the ultimate but a bit slower command. To run both tests,
|
||||
docs build, and flake8 linting, run::
|
||||
|
||||
tox
|
||||
|
||||
This will run exactly the same tests as `Travis CI
|
||||
<https://travis-ci.org/mopidy/mopidy>`_ runs for all our branches and pull
|
||||
requests. If this command turns green, you can be quite confident that your
|
||||
pull request will get the green flag from Travis as well, which is a
|
||||
requirement for it to be merged.
|
||||
|
||||
|
||||
Submitting changes
|
||||
|
||||
@ -12,7 +12,7 @@ Debian-based Linux distributions.
|
||||
Installation
|
||||
============
|
||||
|
||||
See the Debian/Ubuntu section in the :ref:`installation` section.
|
||||
See :ref:`debian-install`.
|
||||
|
||||
|
||||
Running as a system service
|
||||
|
||||
@ -54,7 +54,7 @@ Creating releases
|
||||
#. Update changelog and commit it.
|
||||
|
||||
#. Bump the version number in ``mopidy/__init__.py``. Remember to update the
|
||||
test case in ``tests/version_test.py``.
|
||||
test case in ``tests/test_version.py``.
|
||||
|
||||
#. Merge the release branch (``develop`` in the example) into master::
|
||||
|
||||
|
||||
BIN
docs/ext/api_explorer.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
12
docs/ext/api_explorer.rst
Normal file
@ -0,0 +1,12 @@
|
||||
.. _http-explore-extension:
|
||||
|
||||
Mopidy-API-Explorer
|
||||
===================
|
||||
|
||||
https://github.com/dz0ny/mopidy-api-explorer
|
||||
|
||||
Web extension for browsing the Mopidy HTTP API.
|
||||
|
||||
.. image:: /ext/api_explorer.png
|
||||
:width: 1176
|
||||
:height: 713
|
||||
@ -1,27 +1,14 @@
|
||||
*******************
|
||||
External extensions
|
||||
*******************
|
||||
.. _ext-backends:
|
||||
|
||||
******************
|
||||
Backend extensions
|
||||
******************
|
||||
|
||||
Here you can find a list of external packages that extend Mopidy with
|
||||
additional functionality. This list is moderated and updated on a regular
|
||||
basis. If you want your package to show up here, follow the :ref:`guide on
|
||||
creating extensions <extensiondev>`.
|
||||
additional music sources by implementing the :ref:`backend-api`.
|
||||
|
||||
Mopidy also bundles some extensions:
|
||||
|
||||
- :ref:`ext-local`
|
||||
- :ref:`ext-stream`
|
||||
- :ref:`ext-http`
|
||||
- :ref:`ext-mpd`
|
||||
|
||||
|
||||
Mopidy-Arcam
|
||||
============
|
||||
|
||||
https://github.com/TooDizzy/mopidy-arcam
|
||||
|
||||
Extension for controlling volume using an external Arcam amplifier. Developed
|
||||
and tested with an Arcam AVR-300.
|
||||
This list is moderated and updated on a regular basis. If you want your package
|
||||
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
|
||||
|
||||
|
||||
Mopidy-Beets
|
||||
@ -60,29 +47,10 @@ Extension for playing music and audio from the `Internet Archive
|
||||
<https://archive.org/>`_.
|
||||
|
||||
|
||||
Mopidy-MPRIS
|
||||
Mopidy-Local
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy-mpris
|
||||
|
||||
Extension for controlling Mopidy through the `MPRIS <http://www.mpris.org/>`_
|
||||
D-Bus interface, for example using the Ubuntu Sound Menu.
|
||||
|
||||
|
||||
Mopidy-NAD
|
||||
==========
|
||||
|
||||
https://github.com/mopidy/mopidy-nad
|
||||
|
||||
Extension for controlling volume using an external NAD amplifier.
|
||||
|
||||
|
||||
Mopidy-Notifier
|
||||
===============
|
||||
|
||||
https://github.com/sauberfred/mopidy-notifier
|
||||
|
||||
Extension for displaying track info as User Notifications in Mac OS X.
|
||||
Bundled with Mopidy. See :ref:`ext-local`.
|
||||
|
||||
|
||||
Mopidy-Podcast
|
||||
@ -111,6 +79,24 @@ Extension for Mopidy-Podcast that lets you search and browse podcasts from the
|
||||
Apple iTunes Store.
|
||||
|
||||
|
||||
Mopidy-Podcast-gpodder.net
|
||||
==========================
|
||||
|
||||
https://github.com/tkem/mopidy-podcast-gpodder
|
||||
|
||||
Extension for Mopidy-Podcast that lets you search and browse podcasts from the
|
||||
`gpodder.net <https://gpodder.net/>`_ web site.
|
||||
|
||||
|
||||
Mopidy-Podcast-iTunes
|
||||
=====================
|
||||
|
||||
https://github.com/tkem/mopidy-podcast-itunes
|
||||
|
||||
Extension for Mopidy-Podcast that lets you search and browse podcasts from the
|
||||
Apple iTunes Store.
|
||||
|
||||
|
||||
Mopidy-radio-de
|
||||
===============
|
||||
|
||||
@ -121,14 +107,6 @@ Extension for listening to Internet radio stations and podcasts listed at
|
||||
`radio.fr <http://www.radio.fr/>`_, and `radio.at <http://www.radio.at/>`_.
|
||||
|
||||
|
||||
Mopidy-Scrobbler
|
||||
================
|
||||
|
||||
https://github.com/mopidy/mopidy-scrobbler
|
||||
|
||||
Extension for scrobbling played tracks to Last.fm.
|
||||
|
||||
|
||||
Mopidy-SomaFM
|
||||
=============
|
||||
|
||||
@ -156,6 +134,22 @@ Extension for playing music from the `Spotify <http://www.spotify.com/>`_ music
|
||||
streaming service.
|
||||
|
||||
|
||||
Mopidy-Spotify-Tunigo
|
||||
=====================
|
||||
|
||||
https://github.com/trygveaa/mopidy-spotify-tunigo
|
||||
|
||||
Extension for providing the browse feature of `Spotify
|
||||
<http://www.spotify.com/>`_. This lets you browse playlists, genres and new
|
||||
releases.
|
||||
|
||||
|
||||
Mopidy-Stream
|
||||
=============
|
||||
|
||||
Bundled with Mopidy. See :ref:`ext-stream`.
|
||||
|
||||
|
||||
Mopidy-Subsonic
|
||||
===============
|
||||
|
||||
@ -183,17 +177,8 @@ Provides a backend for playing music from the `VKontakte social network
|
||||
<http://vk.com/>`_.
|
||||
|
||||
|
||||
Mopidy-Yamaha
|
||||
=============
|
||||
|
||||
https://github.com/knutz3n/mopidy-yamaha
|
||||
|
||||
Extension for controlling volume using an external Yamaha network connected
|
||||
amplifier.
|
||||
|
||||
|
||||
Mopidy-YouTube
|
||||
=================
|
||||
==============
|
||||
|
||||
https://github.com/dz0ny/mopidy-youtube
|
||||
|
||||
49
docs/ext/frontends.rst
Normal file
@ -0,0 +1,49 @@
|
||||
.. _ext-frontends:
|
||||
|
||||
*******************
|
||||
Frontend extensions
|
||||
*******************
|
||||
|
||||
Here you can find a list of external packages that extend Mopidy with
|
||||
additional frontends, which includes just about anything that use the
|
||||
:ref:`core-api`.
|
||||
|
||||
This list is moderated and updated on a regular basis. If you want your package
|
||||
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
|
||||
|
||||
|
||||
Mopidy-HTTP
|
||||
===========
|
||||
|
||||
Bundled with Mopidy. See :ref:`ext-http`.
|
||||
|
||||
|
||||
Mopidy-MPD
|
||||
==========
|
||||
|
||||
Bundled with Mopidy. See :ref:`ext-mpd`.
|
||||
|
||||
|
||||
Mopidy-MPRIS
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy-mpris
|
||||
|
||||
Extension for controlling Mopidy through the `MPRIS <http://www.mpris.org/>`_
|
||||
D-Bus interface, for example using the Ubuntu Sound Menu.
|
||||
|
||||
|
||||
Mopidy-Notifier
|
||||
===============
|
||||
|
||||
https://github.com/sauberfred/mopidy-notifier
|
||||
|
||||
Extension for displaying track info as User Notifications in Mac OS X.
|
||||
|
||||
|
||||
Mopidy-Scrobbler
|
||||
================
|
||||
|
||||
https://github.com/mopidy/mopidy-scrobbler
|
||||
|
||||
Extension for scrobbling played tracks to Last.fm.
|
||||
@ -6,7 +6,7 @@ Mopidy-HTTP
|
||||
|
||||
Mopidy-HTTP is an extension that lets you control Mopidy through HTTP and
|
||||
WebSockets, for example from a web client. It is bundled with Mopidy and
|
||||
enabled by default if all dependencies are available.
|
||||
enabled by default.
|
||||
|
||||
When it is enabled it starts a web server at the port specified by the
|
||||
:confval:`http/port` config value.
|
||||
@ -47,24 +47,24 @@ you're looking for a web based client for Mopidy, go check out
|
||||
:ref:`http-clients`.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
Extending the server's functionality
|
||||
====================================
|
||||
|
||||
In addition to Mopidy's dependencies, Mopidy-HTTP requires the following:
|
||||
If you wish to extend the server with additional server side functionality you
|
||||
must create class that implements the :class:`mopidy.http.Router` interface and
|
||||
install it in the extension registry under the ``http:router`` name.
|
||||
|
||||
- cherrypy >= 3.2.2. Available as python-cherrypy3 in Debian/Ubuntu.
|
||||
The default implementation of :class:`mopidy.http.Router` already supports
|
||||
serving static files. If you just want to serve static files you only need to
|
||||
define the class variables :attr:`mopidy.http.Router.name` and
|
||||
:attr:`mopidy.http.Router.path`. For example::
|
||||
|
||||
- ws4py >= 0.2.3. Available as python-ws4py in newer Debian/Ubuntu and from
|
||||
`apt.mopidy.com <http://apt.mopidy.com/>`__ for older releases of
|
||||
Debian/Ubuntu.
|
||||
class MyWebClient(http.Router):
|
||||
name = 'mywebclient'
|
||||
path = os.path.join(os.path.dirname(__file__), 'public_html')
|
||||
|
||||
If you're installing Mopidy with pip, you can run the following command to
|
||||
install Mopidy with the extra dependencies for required for Mopidy-HTTP::
|
||||
|
||||
pip install --upgrade Mopidy[http]
|
||||
|
||||
If you're installing Mopidy from APT, the additional dependencies needed for
|
||||
Mopidy-HTTP are always included.
|
||||
If you wish to extend server with custom methods you can override the method
|
||||
:meth:`mopidy.http.Router.setup_routes` and define custom routes.
|
||||
|
||||
|
||||
Configuration
|
||||
@ -108,4 +108,7 @@ See :ref:`config` for general help on configuring Mopidy.
|
||||
Name of the HTTP service when published through Zeroconf. The variables
|
||||
``$hostname`` and ``$port`` can be used in the name.
|
||||
|
||||
If set, the Zeroconf services ``_http._tcp`` and ``_mopidy-http._tcp`` will
|
||||
be published.
|
||||
|
||||
Set to an empty string to disable Zeroconf for HTTP.
|
||||
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 351 KiB |
10
docs/ext/lux.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Mopidy-Lux
|
||||
==========
|
||||
|
||||
https://github.com/dz0ny/mopidy-lux
|
||||
|
||||
A Mopidy web client made with AngularJS by Janez Troha.
|
||||
|
||||
.. image:: /ext/lux.png
|
||||
:width: 1275
|
||||
:height: 795
|
||||
53
docs/ext/mixers.rst
Normal file
@ -0,0 +1,53 @@
|
||||
.. _ext-mixers:
|
||||
|
||||
****************
|
||||
Mixer extensions
|
||||
****************
|
||||
|
||||
Here you can find a list of external packages that extend Mopidy with
|
||||
additional audio mixers by implementing the :ref:`mixer-api` which was added
|
||||
in Mopidy 0.19.
|
||||
|
||||
This list is moderated and updated on a regular basis. If you want your package
|
||||
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
|
||||
|
||||
|
||||
Mopidy-ALSAMixer
|
||||
================
|
||||
|
||||
https://github.com/mopidy/mopidy-alsamixer
|
||||
|
||||
Extension for controlling volume one a Linux system using ALSA.
|
||||
|
||||
|
||||
Mopidy-Arcam
|
||||
============
|
||||
|
||||
https://github.com/TooDizzy/mopidy-arcam
|
||||
|
||||
Extension for controlling volume using an external Arcam amplifier. Developed
|
||||
and tested with an Arcam AVR-300.
|
||||
|
||||
|
||||
Mopidy-NAD
|
||||
==========
|
||||
|
||||
https://github.com/mopidy/mopidy-nad
|
||||
|
||||
Extension for controlling volume using an external NAD amplifier. Developed
|
||||
and tested with a NAD C355BEE.
|
||||
|
||||
|
||||
Mopidy-SoftwareMixer
|
||||
====================
|
||||
|
||||
Bundled with Mopidy. See :ref:`ext-softwaremixer`.
|
||||
|
||||
|
||||
Mopidy-Yamaha
|
||||
=============
|
||||
|
||||
https://github.com/knutz3n/mopidy-yamaha
|
||||
|
||||
Extension for controlling volume using an external Yamaha network connected
|
||||
amplifier.
|
||||
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
10
docs/ext/moped.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Moped
|
||||
=====
|
||||
|
||||
https://github.com/martijnboland/moped
|
||||
|
||||
A Mopidy web client made with AnbularJS by Martijn Boland.
|
||||
|
||||
.. image:: /ext/moped.png
|
||||
:width: 720
|
||||
:height: 450
|
||||
35
docs/ext/softwaremixer.rst
Normal file
@ -0,0 +1,35 @@
|
||||
.. _ext-softwaremixer:
|
||||
|
||||
********************
|
||||
Mopidy-SoftwareMixer
|
||||
********************
|
||||
|
||||
Mopidy-SoftwareMixer is an extension for controlling audio volume in software
|
||||
through GStreamer. It is the only mixer bundled with Mopidy and is enabled by
|
||||
default.
|
||||
|
||||
If you use PulseAudio, the software mixer will control the per-application
|
||||
volume for Mopidy in PulseAudio, and any changes to the per-application volume
|
||||
done from outside Mopidy will be reflected by the software mixer.
|
||||
|
||||
If you don't use PulseAudio, the mixer will adjust the volume internally in
|
||||
Mopidy's GStreamer pipeline.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Multiple mixers can be installed and enabled at the same time, but only the
|
||||
mixer pointed to by the :confval:`audio/mixer` config value will actually be
|
||||
used.
|
||||
|
||||
See :ref:`config` for general help on configuring Mopidy.
|
||||
|
||||
.. literalinclude:: ../../mopidy/softwaremixer/ext.conf
|
||||
:language: ini
|
||||
|
||||
.. confval:: softwaremixer/enabled
|
||||
|
||||
If the software mixer should be enabled or not. Usually you don't want to
|
||||
change this, but instead change the :confval:`audio/mixer` config value to
|
||||
decide which mixer is actually used.
|
||||
@ -42,3 +42,10 @@ See :ref:`config` for general help on configuring Mopidy.
|
||||
.. confval:: stream/timeout
|
||||
|
||||
Number of milliseconds before giving up looking up stream metadata.
|
||||
|
||||
.. confval:: stream/metadata_blacklist
|
||||
|
||||
List of URI globs to not fetch metadata from before playing. This feature
|
||||
is typically needed for play once URIs provided by certain streaming
|
||||
providers. Regular POSIX glob semantics apply, so ``http://*.example.com/*``
|
||||
would match all example.com sub-domains.
|
||||
|
||||
21
docs/ext/web.rst
Normal file
@ -0,0 +1,21 @@
|
||||
.. _ext-web:
|
||||
|
||||
**************
|
||||
Web extensions
|
||||
**************
|
||||
|
||||
Here you can find a list of external packages that extend Mopidy with
|
||||
additional web interfaces by implementing the :ref:`http-server-api`, which
|
||||
was added in Mopidy 0.19, and optionally using the :ref:`http-api`.
|
||||
|
||||
This list is moderated and updated on a regular basis. If you want your package
|
||||
to show up here, follow the :ref:`guide on creating extensions <extensiondev>`.
|
||||
|
||||
|
||||
.. include:: /ext/api_explorer.rst
|
||||
|
||||
|
||||
.. include:: /ext/lux.rst
|
||||
|
||||
|
||||
.. include:: /ext/moped.rst
|
||||
@ -285,7 +285,7 @@ This is ``mopidy_soundspot/__init__.py``::
|
||||
version = __version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__, 'ext.conf'))
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
@ -413,6 +413,15 @@ more details.
|
||||
return 0
|
||||
|
||||
|
||||
Example web application
|
||||
=======================
|
||||
|
||||
As of Mopidy 0.19, extensions can use Mopidy's builtin web server to host
|
||||
static web clients as well as Tornado and WSGI web applications. For several
|
||||
examples, see the :ref:`http-server-api` docs or explore with
|
||||
:ref:`http-explore-extension` extension.
|
||||
|
||||
|
||||
Example GStreamer element
|
||||
=========================
|
||||
|
||||
@ -423,9 +432,6 @@ Basically, you just implement your GStreamer element in Python and then make
|
||||
your :meth:`~mopidy.ext.Extension.setup` method register all your custom
|
||||
GStreamer elements.
|
||||
|
||||
For examples of custom GStreamer elements implemented in Python, see
|
||||
:mod:`mopidy.audio.mixers`.
|
||||
|
||||
|
||||
Python conventions
|
||||
==================
|
||||
|
||||
@ -33,7 +33,6 @@ Usage
|
||||
:maxdepth: 2
|
||||
|
||||
installation/index
|
||||
installation/raspberrypi
|
||||
config
|
||||
running
|
||||
troubleshooting
|
||||
@ -52,7 +51,11 @@ Extensions
|
||||
ext/stream
|
||||
ext/http
|
||||
ext/mpd
|
||||
ext/external
|
||||
ext/softwaremixer
|
||||
ext/mixers
|
||||
ext/backends
|
||||
ext/frontends
|
||||
ext/web
|
||||
|
||||
|
||||
Clients
|
||||
@ -97,7 +100,7 @@ Reference
|
||||
:maxdepth: 2
|
||||
|
||||
glossary
|
||||
commands/index
|
||||
command
|
||||
api/index
|
||||
modules/index
|
||||
|
||||
|
||||
27
docs/installation/arch.rst
Normal file
@ -0,0 +1,27 @@
|
||||
.. _arch-install:
|
||||
|
||||
****************************
|
||||
Arch Linux: Install from AUR
|
||||
****************************
|
||||
|
||||
If you are running Arch Linux, you can install Mopidy using the
|
||||
`mopidy <https://aur.archlinux.org/packages/mopidy/>`_ package found in AUR.
|
||||
|
||||
#. To install Mopidy with all dependencies, you can use
|
||||
for example `yaourt <https://wiki.archlinux.org/index.php/yaourt>`_::
|
||||
|
||||
yaourt -S mopidy
|
||||
|
||||
To upgrade Mopidy to future releases, just upgrade your system using::
|
||||
|
||||
yaourt -Syu
|
||||
|
||||
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
|
||||
Last.fm scrobbling, AUR also has `packages for several Mopidy extensions
|
||||
<https://aur.archlinux.org/packages/?K=mopidy>`_.
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from AUR, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
64
docs/installation/debian.rst
Normal file
@ -0,0 +1,64 @@
|
||||
.. _debian-install:
|
||||
|
||||
******************************************
|
||||
Debian/Ubuntu: Install from apt.mopidy.com
|
||||
******************************************
|
||||
|
||||
If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
|
||||
install Mopidy is from the `Mopidy APT archive <https://apt.mopidy.com/>`_.
|
||||
When installing from the APT archive, you will automatically get updates to
|
||||
Mopidy in the same way as you get updates to the rest of your system.
|
||||
|
||||
If you're on a Raspberry Pi running Debian or Raspbian, the following
|
||||
instructions should work for you as well. If you're setting up a Raspberry Pi
|
||||
from scratch, we have a guide for installing Debian/Raspbian and Mopidy. See
|
||||
:ref:`raspberrypi-installation`.
|
||||
|
||||
#. Add the archive's GPG key::
|
||||
|
||||
wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||||
|
||||
#. Add the following to ``/etc/apt/sources.list``, or if you have the directory
|
||||
``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in
|
||||
that directory::
|
||||
|
||||
# Mopidy APT archive
|
||||
deb http://apt.mopidy.com/ stable main contrib non-free
|
||||
deb-src http://apt.mopidy.com/ stable main contrib non-free
|
||||
|
||||
For the lazy, you can simply run the following command to create
|
||||
``/etc/apt/sources.list.d/mopidy.list``::
|
||||
|
||||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/mopidy.list
|
||||
|
||||
#. Install Mopidy and all dependencies::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
|
||||
Last.fm scrobbling, you need to install additional packages.
|
||||
|
||||
To list all the extensions available from apt.mopidy.com, you can run::
|
||||
|
||||
apt-cache search mopidy
|
||||
|
||||
To install one of the listed packages, e.g. ``mopidy-spotify``, simply run::
|
||||
|
||||
sudo apt-get install mopidy-spotify
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from apt.mopidy.com, see :ref:`ext`.
|
||||
|
||||
#. Before continuing, make sure you've read the :ref:`debian` section to learn
|
||||
about the differences between running Mopidy as a system service and
|
||||
manually as your own system user.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
When a new release of Mopidy is out, and you can't wait for you system to
|
||||
figure it out for itself, run the following to upgrade right away::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get dist-upgrade
|
||||
@ -5,295 +5,15 @@ Installation
|
||||
************
|
||||
|
||||
There are several ways to install Mopidy. What way is best depends upon your OS
|
||||
and/or distribution. If you want to contribute to the development of Mopidy,
|
||||
you should first read this page, then have a look at :ref:`run-from-git`.
|
||||
and/or distribution.
|
||||
|
||||
.. contents:: Installation guides
|
||||
:local:
|
||||
If you want to contribute to the development of Mopidy, you should first read
|
||||
the general installation instructions, then have a look at :ref:`run-from-git`.
|
||||
|
||||
.. toctree::
|
||||
|
||||
Debian/Ubuntu: Install from apt.mopidy.com
|
||||
==========================================
|
||||
|
||||
If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
|
||||
install Mopidy is from the `Mopidy APT archive <http://apt.mopidy.com/>`_. When
|
||||
installing from the APT archive, you will automatically get updates to Mopidy
|
||||
in the same way as you get updates to the rest of your distribution.
|
||||
|
||||
#. Add the archive's GPG key::
|
||||
|
||||
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||||
|
||||
#. Add the following to ``/etc/apt/sources.list``, or if you have the directory
|
||||
``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in
|
||||
that directory::
|
||||
|
||||
# Mopidy APT archive
|
||||
deb http://apt.mopidy.com/ stable main contrib non-free
|
||||
deb-src http://apt.mopidy.com/ stable main contrib non-free
|
||||
|
||||
For the lazy, you can simply run the following command to create
|
||||
``/etc/apt/sources.list.d/mopidy.list``::
|
||||
|
||||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
|
||||
|
||||
#. Install Mopidy and all dependencies::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
Note that this will only install the main Mopidy package. For e.g. Spotify
|
||||
or SoundCloud support you need to install the respective extension packages.
|
||||
To list all the extensions available from apt.mopidy.com, you can run::
|
||||
|
||||
apt-cache search mopidy
|
||||
|
||||
To install one of the listed packages, e.g. ``mopidy-spotify``, simply run::
|
||||
|
||||
sudo apt-get install mopidy-spotify
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from apt.mopidy.com, see :ref:`ext`.
|
||||
|
||||
#. Before continuing, make sure you've read the :ref:`debian` section to learn
|
||||
about the differences between running Mopidy as a system service and
|
||||
manually as your own system user.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
When a new release of Mopidy is out, and you can't wait for you system to
|
||||
figure it out for itself, run the following to upgrade right away::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get dist-upgrade
|
||||
|
||||
|
||||
Raspberry Pi running Debian
|
||||
---------------------------
|
||||
|
||||
We have a guide for installing a Raspberry Pi from scratch with Debian/Raspbian
|
||||
and Mopidy. See :ref:`raspberrypi-installation`.
|
||||
|
||||
|
||||
Arch Linux: Install from AUR
|
||||
============================
|
||||
|
||||
If you are running Arch Linux, you can install Mopidy
|
||||
using the `mopidy <https://aur.archlinux.org/packages/mopidy/>`_
|
||||
package found in AUR.
|
||||
|
||||
#. To install Mopidy with all dependencies, you can use
|
||||
for example `yaourt <https://wiki.archlinux.org/index.php/yaourt>`_::
|
||||
|
||||
yaourt -S mopidy
|
||||
|
||||
To upgrade Mopidy to future releases, just upgrade your system using::
|
||||
|
||||
yaourt -Syu
|
||||
|
||||
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
|
||||
Last.fm scrobbling, AUR also has `packages for several Mopidy extensions
|
||||
<https://aur.archlinux.org/packages/?K=mopidy>`_.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
OS X: Install from Homebrew and pip
|
||||
===================================
|
||||
|
||||
If you are running OS X, you can install everything needed with Homebrew and
|
||||
pip.
|
||||
|
||||
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
|
||||
|
||||
If you are already using Homebrew, make sure your installation is up to
|
||||
date before you continue::
|
||||
|
||||
brew update
|
||||
brew upgrade
|
||||
|
||||
#. Mopidy requires GStreamer 0.10, but Homebrew's main formula repo has
|
||||
upgraded its GStreamer packages to 1.0. Thus, you'll need to add an
|
||||
alternative formula repo (aka "tap") that has the old GStreamer formulas::
|
||||
|
||||
brew tap homebrew/versions
|
||||
|
||||
#. Install the required packages from Homebrew::
|
||||
|
||||
brew install gst-python010 gst-plugins-good010 gst-plugins-ugly010
|
||||
|
||||
#. Make sure to include Homebrew's Python ``site-packages`` directory in your
|
||||
``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
|
||||
and it will crash.
|
||||
|
||||
You can either amend your ``PYTHONPATH`` permanently, by adding the
|
||||
following statement to your shell's init file, e.g. ``~/.bashrc``::
|
||||
|
||||
export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
|
||||
|
||||
Or, you can prefix the Mopidy command every time you run it::
|
||||
|
||||
PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
|
||||
|
||||
#. Next up, you need to install some Python packages. To do so, we use pip. If
|
||||
you don't have the ``pip`` command, you can install it now::
|
||||
|
||||
sudo easy_install pip
|
||||
|
||||
#. Then, install the latest release of Mopidy using pip::
|
||||
|
||||
sudo pip install -U mopidy
|
||||
|
||||
#. Optionally, install additional extensions to Mopidy.
|
||||
|
||||
For HTTP frontend support, so you can run Mopidy web clients::
|
||||
|
||||
sudo pip install -U mopidy[http]
|
||||
|
||||
For playing music from Spotify::
|
||||
|
||||
brew install libspotify
|
||||
sudo pip install -U mopidy-spotify
|
||||
|
||||
For scrobbling to Last.fm::
|
||||
|
||||
sudo pip install -U mopidy-scrobbler
|
||||
|
||||
For more extensions, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
Otherwise: Install from source using pip
|
||||
========================================
|
||||
|
||||
If you are on on Linux, but can't install from the APT archive or from AUR, you
|
||||
can install Mopidy from PyPI using pip.
|
||||
|
||||
#. First of all, you need Python 2.7. Check if you have Python and what
|
||||
version by running::
|
||||
|
||||
python --version
|
||||
|
||||
#. When you install using pip, you need to make sure you have pip. You'll also
|
||||
need a C compiler and the Python development headers to build pyspotify
|
||||
later.
|
||||
|
||||
This is how you install it on Debian/Ubuntu::
|
||||
|
||||
sudo apt-get install build-essential python-dev python-pip
|
||||
|
||||
And on Arch Linux from the official repository::
|
||||
|
||||
sudo pacman -S base-devel python2-pip
|
||||
|
||||
And on Fedora Linux from the official repositories::
|
||||
|
||||
sudo yum install -y gcc python-devel python-pip
|
||||
|
||||
.. note::
|
||||
|
||||
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
|
||||
following steps.
|
||||
|
||||
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python
|
||||
bindings. GStreamer is packaged for most popular Linux distributions. Search
|
||||
for GStreamer in your package manager, and make sure to install the Python
|
||||
bindings, and the "good" and "ugly" plugin sets.
|
||||
|
||||
If you use Debian/Ubuntu you can install GStreamer like this::
|
||||
|
||||
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
If you use Arch Linux, install the following packages from the official
|
||||
repository::
|
||||
|
||||
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
||||
gstreamer0.10-ugly-plugins
|
||||
|
||||
If you use Fedora you can install GStreamer like this::
|
||||
|
||||
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
If you use Gentoo you need to be careful because GStreamer 0.10 is in a
|
||||
different lower slot than 1.0, the default. Your emerge commands will need
|
||||
to include the slot::
|
||||
|
||||
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \
|
||||
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
|
||||
|
||||
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you
|
||||
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc.
|
||||
|
||||
#. Install the latest release of Mopidy::
|
||||
|
||||
sudo pip install -U mopidy
|
||||
|
||||
To upgrade Mopidy to future releases, just rerun this command.
|
||||
|
||||
Alternatively, if you want to track Mopidy development closer, you may
|
||||
install a snapshot of Mopidy's ``develop`` Git branch using pip::
|
||||
|
||||
sudo pip install --allow-unverified=mopidy mopidy==dev
|
||||
|
||||
#. Optional: If you want to use the HTTP frontend and web clients, you need
|
||||
some additional dependencies::
|
||||
|
||||
sudo pip install -U mopidy[http]
|
||||
|
||||
#. Optional: If you want Spotify support in Mopidy, you'll need to install
|
||||
libspotify and the Mopidy-Spotify extension.
|
||||
|
||||
#. Download and install the latest version of libspotify for your OS and CPU
|
||||
architecture from `Spotify
|
||||
<https://developer.spotify.com/technologies/libspotify/>`_.
|
||||
|
||||
For libspotify 12.1.51 for 64-bit Linux the process is as follows::
|
||||
|
||||
wget https://developer.spotify.com/download/libspotify/libspotify-12.1.51-Linux-x86_64-release.tar.gz
|
||||
tar zxfv libspotify-12.1.51-Linux-x86_64-release.tar.gz
|
||||
cd libspotify-12.1.51-Linux-x86_64-release/
|
||||
sudo make install prefix=/usr/local
|
||||
|
||||
Remember to adjust the above example for the latest libspotify version
|
||||
supported by pyspotify, your OS, and your CPU architecture.
|
||||
|
||||
#. If you're on Fedora, you must add a configuration file so libspotify.so
|
||||
can be found::
|
||||
|
||||
echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/libspotify.conf
|
||||
sudo ldconfig
|
||||
|
||||
#. Then install the latest release of Mopidy-Spotify using pip::
|
||||
|
||||
sudo pip install -U mopidy-spotify
|
||||
|
||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
||||
to install Mopidy-Scrobbler::
|
||||
|
||||
sudo pip install -U mopidy-scrobbler
|
||||
|
||||
#. Optional: To use Mopidy-MPRIS, e.g. for controlling Mopidy from the Ubuntu
|
||||
Sound Menu or from an UPnP client via Rygel, you need some additional
|
||||
dependencies and the Mopidy-MPRIS extension.
|
||||
|
||||
#. Install the Python bindings for libindicate, and the Python bindings for
|
||||
libdbus, the reference D-Bus library.
|
||||
|
||||
On Debian/Ubuntu::
|
||||
|
||||
sudo apt-get install python-dbus python-indicate
|
||||
|
||||
#. Then install the latest release of Mopidy-MPRIS using pip::
|
||||
|
||||
sudo pip install -U mopidy-mpris
|
||||
|
||||
#. For more Mopidy extensions, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
debian
|
||||
arch
|
||||
osx
|
||||
source
|
||||
raspberrypi
|
||||
|
||||
65
docs/installation/osx.rst
Normal file
@ -0,0 +1,65 @@
|
||||
***************************
|
||||
OS X: Install from Homebrew
|
||||
***************************
|
||||
|
||||
If you are running OS X, you can install everything needed with Homebrew.
|
||||
|
||||
#. Install Xcode command line developer tools. Do this even if you already have
|
||||
Xcode installed::
|
||||
|
||||
xcode-select --install
|
||||
|
||||
#. Install `XQuartz <http://xquartz.macosforge.org/>`_. This is needed by
|
||||
GStreamer which Mopidy use heavily.
|
||||
|
||||
#. Install `Homebrew <https://github.com/Homebrew/homebrew>`_.
|
||||
|
||||
#. If you are already using Homebrew, make sure your installation is up to
|
||||
date before you continue::
|
||||
|
||||
brew update
|
||||
brew upgrade
|
||||
|
||||
#. Mopidy works out of box if you have installed Python from Homebrew::
|
||||
|
||||
brew install python
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to use the Python version bundled with OS X, you'll need to
|
||||
include Python packages installed by Homebrew in your ``PYTHONPATH``.
|
||||
If you don't do this, the ``mopidy`` executable will not find its
|
||||
dependencies and will crash.
|
||||
|
||||
You can either amend your ``PYTHONPATH`` permanently, by adding the
|
||||
following statement to your shell's init file, e.g. ``~/.bashrc``::
|
||||
|
||||
export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
|
||||
|
||||
Or, you can prefix the Mopidy command every time you run it::
|
||||
|
||||
PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
|
||||
|
||||
#. Mopidy has its own `Homebrew formula repo
|
||||
<https://github.com/mopidy/homebrew-mopidy>`_, called a "tap". To enable our
|
||||
Homebrew tap, run::
|
||||
|
||||
brew tap mopidy/mopidy
|
||||
|
||||
#. To install Mopidy, run::
|
||||
|
||||
brew install mopidy
|
||||
|
||||
#. Optional: If you want to use any Mopidy extensions, like Spotify support or
|
||||
Last.fm scrobbling, the Homebrew tap has formulas for several Mopidy
|
||||
extensions as well.
|
||||
|
||||
To list all the extensions available from our tap, you can run::
|
||||
|
||||
brew search mopidy
|
||||
|
||||
For a full list of available Mopidy extensions, including those not
|
||||
installable from Homebrew, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
@ -1,8 +1,8 @@
|
||||
.. _raspberrypi-installation:
|
||||
|
||||
****************************
|
||||
Installation on Raspberry Pi
|
||||
****************************
|
||||
*************************************
|
||||
Raspberry Pi: Mopidy on a credit card
|
||||
*************************************
|
||||
|
||||
Mopidy runs nicely on a `Raspberry Pi <http://www.raspberrypi.org/>`_. As of
|
||||
January 2013, Mopidy will run with Spotify support on both the armel
|
||||
@ -71,11 +71,12 @@ you a lot better performance.
|
||||
command to e.g. ``/etc/rc.local``, which will be executed when the system is
|
||||
booting.
|
||||
|
||||
#. Install Mopidy and its dependencies from `apt.mopidy.com
|
||||
<http://apt.mopidy.com/>`_, as described in :ref:`installation`.
|
||||
#. Install Mopidy and its dependencies as described in :ref:`debian-install`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
then you're ready to :doc:`run Mopidy </running>`. Alternatively you may
|
||||
want to have Mopidy run as a :doc:`system service </debian>`, automatically
|
||||
starting at boot.
|
||||
|
||||
|
||||
Appendix: Fixing audio quality issues
|
||||
|
||||
114
docs/installation/source.rst
Normal file
@ -0,0 +1,114 @@
|
||||
.. _source-install:
|
||||
|
||||
*******************
|
||||
Install from source
|
||||
*******************
|
||||
|
||||
If you are on Linux, but can't install :ref:`from the APT archive
|
||||
<debian-install>` or :ref:`from AUR <arch-install>`, you can install Mopidy
|
||||
from source by hand.
|
||||
|
||||
#. First of all, you need Python 2.7. Check if you have Python and what
|
||||
version by running::
|
||||
|
||||
python --version
|
||||
|
||||
#. You need to make sure you have ``pip``, the Python package installer. You'll
|
||||
also need a C compiler and the Python development headers to build pyspotify
|
||||
later.
|
||||
|
||||
This is how you install it on Debian/Ubuntu::
|
||||
|
||||
sudo apt-get install build-essential python-dev python-pip
|
||||
|
||||
And on Arch Linux from the official repository::
|
||||
|
||||
sudo pacman -S base-devel python2-pip
|
||||
|
||||
And on Fedora Linux from the official repositories::
|
||||
|
||||
sudo yum install -y gcc python-devel python-pip
|
||||
|
||||
.. note::
|
||||
|
||||
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
|
||||
following steps.
|
||||
|
||||
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python
|
||||
bindings. GStreamer is packaged for most popular Linux distributions. Search
|
||||
for GStreamer in your package manager, and make sure to install the Python
|
||||
bindings, and the "good" and "ugly" plugin sets.
|
||||
|
||||
If you use Debian/Ubuntu you can install GStreamer like this::
|
||||
|
||||
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
If you use Arch Linux, install the following packages from the official
|
||||
repository::
|
||||
|
||||
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
||||
gstreamer0.10-ugly-plugins
|
||||
|
||||
If you use Fedora you can install GStreamer like this::
|
||||
|
||||
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
If you use Gentoo you need to be careful because GStreamer 0.10 is in a
|
||||
different lower slot than 1.0, the default. Your emerge commands will need
|
||||
to include the slot::
|
||||
|
||||
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \
|
||||
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
|
||||
|
||||
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you
|
||||
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc.
|
||||
|
||||
#. Install the latest release of Mopidy::
|
||||
|
||||
sudo pip install -U mopidy
|
||||
|
||||
To upgrade Mopidy to future releases, just rerun this command.
|
||||
|
||||
Alternatively, if you want to track Mopidy development closer, you may
|
||||
install a snapshot of Mopidy's ``develop`` Git branch using pip::
|
||||
|
||||
sudo pip install --allow-unverified=mopidy mopidy==dev
|
||||
|
||||
#. Optional: If you want Spotify support in Mopidy, you'll need to install
|
||||
libspotify and the Mopidy-Spotify extension.
|
||||
|
||||
#. Download and install the latest version of libspotify for your OS and CPU
|
||||
architecture from `Spotify
|
||||
<https://developer.spotify.com/technologies/libspotify/>`_.
|
||||
|
||||
For libspotify 12.1.51 for 64-bit Linux the process is as follows::
|
||||
|
||||
wget https://developer.spotify.com/download/libspotify/libspotify-12.1.51-Linux-x86_64-release.tar.gz
|
||||
tar zxfv libspotify-12.1.51-Linux-x86_64-release.tar.gz
|
||||
cd libspotify-12.1.51-Linux-x86_64-release/
|
||||
sudo make install prefix=/usr/local
|
||||
|
||||
Remember to adjust the above example for the latest libspotify version
|
||||
supported by pyspotify, your OS, and your CPU architecture.
|
||||
|
||||
#. If you're on Fedora, you must add a configuration file so libspotify.so
|
||||
can be found::
|
||||
|
||||
echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/libspotify.conf
|
||||
sudo ldconfig
|
||||
|
||||
#. Then install the latest release of Mopidy-Spotify using pip::
|
||||
|
||||
sudo pip install -U mopidy-spotify
|
||||
|
||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
||||
to install Mopidy-Scrobbler::
|
||||
|
||||
sudo pip install -U mopidy-scrobbler
|
||||
|
||||
#. For a full list of available Mopidy extensions, see :ref:`ext`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
@ -7,6 +7,13 @@ For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
|
||||
.. automodule:: mopidy.mpd
|
||||
:synopsis: MPD server frontend
|
||||
|
||||
MPD tokenizer
|
||||
=============
|
||||
|
||||
.. automodule:: mopidy.mpd.tokenize
|
||||
:synopsis: MPD request tokenizer
|
||||
:members:
|
||||
|
||||
|
||||
MPD dispatcher
|
||||
==============
|
||||
|
||||
@ -7,7 +7,7 @@ Troubleshooting
|
||||
If you run into problems with Mopidy, we usually hang around at ``#mopidy`` at
|
||||
`irc.freenode.net <http://freenode.net/>`_ and also have a `mailing list at
|
||||
Google Groups <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_.
|
||||
If you stumble into a bug or got a feature request, please create an issue in
|
||||
If you stumble into a bug or have a feature request, please create an issue in
|
||||
the `issue tracker <https://github.com/mopidy/mopidy/issues>`_.
|
||||
|
||||
When you're debugging yourself or asking for help, there are some tools built
|
||||
@ -64,7 +64,7 @@ docs for the :confval:`loglevels/*` config section.
|
||||
Debugging deadlocks
|
||||
===================
|
||||
|
||||
If Mopidy hangs without and obvious explanation, you can send the ``SIGUSR1``
|
||||
If Mopidy hangs without an obvious explanation, you can send the ``SIGUSR1``
|
||||
signal to the Mopidy process. If Mopidy's main thread is still responsive, it
|
||||
will log a traceback for each running thread, showing what the threads are
|
||||
currently doing. This is a very useful tool for understanding exactly how the
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON("package.json"),
|
||||
meta: {
|
||||
banner: "/*! Mopidy.js - built " +
|
||||
banner: "/*! Mopidy.js v<%= pkg.version %> - built " +
|
||||
"<%= grunt.template.today('yyyy-mm-dd') %>\n" +
|
||||
" * http://www.mopidy.com/\n" +
|
||||
" * Copyright (c) <%= grunt.template.today('yyyy') %> " +
|
||||
@ -26,7 +27,7 @@ module.exports = function (grunt) {
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
next(err, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
@ -45,7 +46,7 @@ module.exports = function (grunt) {
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
next(err, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
|
||||
32
js/README.md
@ -41,20 +41,15 @@ After npm completes, you can import Mopidy.js using ``require()``:
|
||||
Using the library
|
||||
-----------------
|
||||
|
||||
See Mopidy's [HTTP API
|
||||
documentation](http://docs.mopidy.com/en/latest/api/http/).
|
||||
See the [Mopidy.js documentation](http://docs.mopidy.com/en/latest/api/js/).
|
||||
|
||||
|
||||
Building from source
|
||||
--------------------
|
||||
|
||||
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're
|
||||
running Ubuntu:
|
||||
1. Install [Node.js](http://nodejs.org/) and npm. If you're running Ubuntu:
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs
|
||||
sudo apt-get install nodejs-legacy npm
|
||||
|
||||
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
|
||||
|
||||
@ -85,6 +80,27 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in
|
||||
Changelog
|
||||
---------
|
||||
|
||||
### 0.4.0 (2014-06-24)
|
||||
|
||||
- Add support for method calls with by-name arguments. The old calling
|
||||
convention, "by-position-only", is still the default, but this will change in
|
||||
the future. A warning is printed to the console if you don't explicitly
|
||||
select a calling convention. See the docs for details.
|
||||
|
||||
### 0.3.0 (2014-06-16)
|
||||
|
||||
- Upgrade to when.js 3, which brings great performance improvements and better
|
||||
debugging facilities. If you maintain a Mopidy client, you should review the
|
||||
[differences between when.js 2 and 3](https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x)
|
||||
and the
|
||||
[when.js debugging guide](https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises).
|
||||
|
||||
- All promise rejection values are now of the Error type. This ensures that all
|
||||
JavaScript VMs will show a useful stack trace if a rejected promise's value
|
||||
is used to throw an exception. To allow catch clauses to handle different
|
||||
errors differently, server side errors are of the type `Mopidy.ServerError`,
|
||||
and connection related errors are of the type `Mopidy.ConnectionError`.
|
||||
|
||||
### 0.2.0 (2014-01-04)
|
||||
|
||||
- **Backwards incompatible change for Node.js users:**
|
||||
|
||||
@ -1,36 +1,60 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "Client lib for controlling a Mopidy music server over a WebSocket",
|
||||
"keywords": [
|
||||
"mopidy",
|
||||
"music",
|
||||
"client",
|
||||
"websocket",
|
||||
"json-rpc"
|
||||
],
|
||||
"homepage": "http://www.mopidy.com/",
|
||||
"bugs": "https://github.com/mopidy/mopidy/issues",
|
||||
"license": "Apache-2.0",
|
||||
"author": {
|
||||
"name": "Stein Magnus Jodal",
|
||||
"email": "stein.magnus@jodal.no",
|
||||
"url": "http://www.jodal.no"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Stein Magnus Jodal",
|
||||
"email": "stein.magnus@jodal.no",
|
||||
"url": "http://www.jodal.no"
|
||||
},
|
||||
{
|
||||
"name": "Paul Connolley",
|
||||
"email": "paul.connolley@gmail.com"
|
||||
}
|
||||
],
|
||||
"main": "src/mopidy.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/mopidy/mopidy.git"
|
||||
},
|
||||
"main": "src/mopidy.js",
|
||||
"dependencies": {
|
||||
"bane": "~1.1.0",
|
||||
"faye-websocket": "~0.7.2",
|
||||
"when": "~2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.7.8",
|
||||
"grunt": "~0.4.2",
|
||||
"grunt-buster": "~0.3.1",
|
||||
"grunt-browserify": "~1.3.0",
|
||||
"grunt-contrib-jshint": "~0.8.0",
|
||||
"grunt-contrib-uglify": "~0.2.7",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"phantomjs": "~1.9.2-6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"build": "grunt build",
|
||||
"start": "grunt watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"bane": "~1.1.0",
|
||||
"faye-websocket": "~0.7.2",
|
||||
"when": "~3.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.7.13",
|
||||
"browserify": "~3",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-buster": "~0.3.1",
|
||||
"grunt-browserify": "~1.3.2",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-uglify": "~0.5.0",
|
||||
"grunt-contrib-watch": "~0.6.1",
|
||||
"phantomjs": "~1.9.7-8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
}
|
||||
|
||||
127
js/src/mopidy.js
@ -9,8 +9,8 @@ function Mopidy(settings) {
|
||||
return new Mopidy(settings);
|
||||
}
|
||||
|
||||
this._console = this._getConsole(settings || {});
|
||||
this._settings = this._configure(settings || {});
|
||||
this._console = this._getConsole();
|
||||
|
||||
this._backoffDelay = this._settings.backoffDelayMin;
|
||||
this._pendingRequests = {};
|
||||
@ -24,13 +24,41 @@ function Mopidy(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
Mopidy.ConnectionError = function (message) {
|
||||
this.name = "ConnectionError";
|
||||
this.message = message;
|
||||
};
|
||||
Mopidy.ConnectionError.prototype = new Error();
|
||||
Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError;
|
||||
|
||||
Mopidy.ServerError = function (message) {
|
||||
this.name = "ServerError";
|
||||
this.message = message;
|
||||
};
|
||||
Mopidy.ServerError.prototype = new Error();
|
||||
Mopidy.ServerError.prototype.constructor = Mopidy.ServerError;
|
||||
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
|
||||
Mopidy.prototype._getConsole = function (settings) {
|
||||
if (typeof settings.console !== "undefined") {
|
||||
return settings.console;
|
||||
}
|
||||
|
||||
var con = typeof console !== "undefined" && console || {};
|
||||
|
||||
con.log = con.log || function () {};
|
||||
con.warn = con.warn || function () {};
|
||||
con.error = con.error || function () {};
|
||||
|
||||
return con;
|
||||
};
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
var currentHost = (typeof document !== "undefined" &&
|
||||
document.location.host) || "localhost";
|
||||
settings.webSocketUrl = settings.webSocketUrl ||
|
||||
"ws://" + currentHost + "/mopidy/ws/";
|
||||
"ws://" + currentHost + "/mopidy/ws";
|
||||
|
||||
if (settings.autoConnect !== false) {
|
||||
settings.autoConnect = true;
|
||||
@ -39,19 +67,18 @@ Mopidy.prototype._configure = function (settings) {
|
||||
settings.backoffDelayMin = settings.backoffDelayMin || 1000;
|
||||
settings.backoffDelayMax = settings.backoffDelayMax || 64000;
|
||||
|
||||
if (typeof settings.callingConvention === "undefined") {
|
||||
this._console.warn(
|
||||
"Mopidy.js is using the default calling convention. The " +
|
||||
"default will change in the future. You should explicitly " +
|
||||
"specify which calling convention you use.");
|
||||
}
|
||||
settings.callingConvention = (
|
||||
settings.callingConvention || "by-position-only");
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
Mopidy.prototype._getConsole = function () {
|
||||
var console = typeof console !== "undefined" && console || {};
|
||||
|
||||
console.log = console.log || function () {};
|
||||
console.warn = console.warn || function () {};
|
||||
console.error = console.error || function () {};
|
||||
|
||||
return console;
|
||||
};
|
||||
|
||||
Mopidy.prototype._delegateEvents = function () {
|
||||
// Remove existing event handlers
|
||||
this.off("websocket:close");
|
||||
@ -102,10 +129,9 @@ Mopidy.prototype._cleanup = function (closeEvent) {
|
||||
Object.keys(this._pendingRequests).forEach(function (requestId) {
|
||||
var resolver = this._pendingRequests[requestId];
|
||||
delete this._pendingRequests[requestId];
|
||||
resolver.reject({
|
||||
message: "WebSocket closed",
|
||||
closeEvent: closeEvent
|
||||
});
|
||||
var error = new Mopidy.ConnectionError("WebSocket closed");
|
||||
error.closeEvent = closeEvent;
|
||||
resolver.reject(error);
|
||||
}.bind(this));
|
||||
|
||||
this.emit("state:offline");
|
||||
@ -141,33 +167,25 @@ Mopidy.prototype._handleWebSocketError = function (error) {
|
||||
};
|
||||
|
||||
Mopidy.prototype._send = function (message) {
|
||||
var deferred = when.defer();
|
||||
|
||||
switch (this._webSocket.readyState) {
|
||||
case Mopidy.WebSocket.CONNECTING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is still connecting"
|
||||
});
|
||||
break;
|
||||
return when.reject(
|
||||
new Mopidy.ConnectionError("WebSocket is still connecting"));
|
||||
case Mopidy.WebSocket.CLOSING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closing"
|
||||
});
|
||||
break;
|
||||
return when.reject(
|
||||
new Mopidy.ConnectionError("WebSocket is closing"));
|
||||
case Mopidy.WebSocket.CLOSED:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closed"
|
||||
});
|
||||
break;
|
||||
return when.reject(
|
||||
new Mopidy.ConnectionError("WebSocket is closed"));
|
||||
default:
|
||||
var deferred = when.defer();
|
||||
message.jsonrpc = "2.0";
|
||||
message.id = this._nextRequestId();
|
||||
this._pendingRequests[message.id] = deferred.resolver;
|
||||
this._webSocket.send(JSON.stringify(message));
|
||||
this.emit("websocket:outgoingMessage", message);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
Mopidy.prototype._nextRequestId = (function () {
|
||||
@ -208,19 +226,22 @@ Mopidy.prototype._handleResponse = function (responseMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
var error;
|
||||
var resolver = this._pendingRequests[responseMessage.id];
|
||||
delete this._pendingRequests[responseMessage.id];
|
||||
|
||||
if (responseMessage.hasOwnProperty("result")) {
|
||||
resolver.resolve(responseMessage.result);
|
||||
} else if (responseMessage.hasOwnProperty("error")) {
|
||||
resolver.reject(responseMessage.error);
|
||||
error = new Mopidy.ServerError(responseMessage.error.message);
|
||||
error.code = responseMessage.error.code;
|
||||
error.data = responseMessage.error.data;
|
||||
resolver.reject(error);
|
||||
this._console.warn("Server returned error:", responseMessage.error);
|
||||
} else {
|
||||
resolver.reject({
|
||||
message: "Response without 'result' or 'error' received",
|
||||
data: {response: responseMessage}
|
||||
});
|
||||
error = new Error("Response without 'result' or 'error' received");
|
||||
error.data = {response: responseMessage};
|
||||
resolver.reject(error);
|
||||
this._console.warn(
|
||||
"Response without 'result' or 'error' received. Message was:",
|
||||
responseMessage);
|
||||
@ -237,18 +258,36 @@ Mopidy.prototype._handleEvent = function (eventMessage) {
|
||||
|
||||
Mopidy.prototype._getApiSpec = function () {
|
||||
return this._send({method: "core.describe"})
|
||||
.then(this._createApi.bind(this), this._handleWebSocketError)
|
||||
.then(null, this._handleWebSocketError);
|
||||
.then(this._createApi.bind(this))
|
||||
.catch(this._handleWebSocketError);
|
||||
};
|
||||
|
||||
Mopidy.prototype._createApi = function (methods) {
|
||||
var byPositionOrByName = (
|
||||
this._settings.callingConvention === "by-position-or-by-name");
|
||||
|
||||
var caller = function (method) {
|
||||
return function () {
|
||||
var params = Array.prototype.slice.call(arguments);
|
||||
return this._send({
|
||||
method: method,
|
||||
params: params
|
||||
});
|
||||
var message = {method: method};
|
||||
if (arguments.length === 0) {
|
||||
return this._send(message);
|
||||
}
|
||||
if (!byPositionOrByName) {
|
||||
message.params = Array.prototype.slice.call(arguments);
|
||||
return this._send(message);
|
||||
}
|
||||
if (arguments.length > 1) {
|
||||
return when.reject(new Error(
|
||||
"Expected zero arguments, a single array, " +
|
||||
"or a single object."));
|
||||
}
|
||||
if (!Array.isArray(arguments[0]) &&
|
||||
arguments[0] !== Object(arguments[0])) {
|
||||
return when.reject(new TypeError(
|
||||
"Expected an array or an object."));
|
||||
}
|
||||
message.params = arguments[0];
|
||||
return this._send(message);
|
||||
}.bind(this);
|
||||
}.bind(this);
|
||||
|
||||
|
||||
@ -33,7 +33,10 @@ buster.testCase("Mopidy", {
|
||||
close: this.stub(),
|
||||
send: this.stub()
|
||||
};
|
||||
this.mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
this.mopidy = new Mopidy({
|
||||
callingConvention: "by-position-or-by-name",
|
||||
webSocket: this.webSocket
|
||||
});
|
||||
},
|
||||
|
||||
tearDown: function () {
|
||||
@ -42,31 +45,86 @@ buster.testCase("Mopidy", {
|
||||
|
||||
"constructor": {
|
||||
"connects when autoConnect is true": function () {
|
||||
new Mopidy({autoConnect: true});
|
||||
new Mopidy({
|
||||
autoConnect: true,
|
||||
callingConvention: "by-position-or-by-name"
|
||||
});
|
||||
|
||||
var currentHost = typeof document !== "undefined" &&
|
||||
document.location.host || "localhost";
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + currentHost + "/mopidy/ws/");
|
||||
"ws://" + currentHost + "/mopidy/ws");
|
||||
},
|
||||
|
||||
"does not connect when autoConnect is false": function () {
|
||||
new Mopidy({autoConnect: false});
|
||||
new Mopidy({
|
||||
autoConnect: false,
|
||||
callingConvention: "by-position-or-by-name"
|
||||
});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
},
|
||||
|
||||
"does not connect when passed a WebSocket": function () {
|
||||
new Mopidy({webSocket: {}});
|
||||
new Mopidy({
|
||||
callingConvention: "by-position-or-by-name",
|
||||
webSocket: {}
|
||||
});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
},
|
||||
|
||||
"defaults to by-position-only calling convention": function () {
|
||||
var console = {
|
||||
warn: function () {}
|
||||
};
|
||||
var mopidy = new Mopidy({
|
||||
console: console,
|
||||
webSocket: this.webSocket,
|
||||
});
|
||||
|
||||
assert.equals(
|
||||
mopidy._settings.callingConvention,
|
||||
"by-position-only");
|
||||
},
|
||||
|
||||
"warns if no calling convention explicitly selected": function () {
|
||||
var console = {
|
||||
warn: function () {}
|
||||
};
|
||||
var stub = this.stub(console, "warn");
|
||||
|
||||
new Mopidy({console: console});
|
||||
|
||||
assert.calledOnceWith(
|
||||
stub,
|
||||
"Mopidy.js is using the default calling convention. The " +
|
||||
"default will change in the future. You should explicitly " +
|
||||
"specify which calling convention you use.");
|
||||
},
|
||||
|
||||
"does not warn if calling convention chosen explicitly": function () {
|
||||
var console = {
|
||||
warn: function () {}
|
||||
};
|
||||
var stub = this.stub(console, "warn");
|
||||
|
||||
new Mopidy({
|
||||
callingConvention: "by-position-or-by-name",
|
||||
console: console
|
||||
});
|
||||
|
||||
refute.called(stub);
|
||||
},
|
||||
|
||||
"works without 'new' keyword": function () {
|
||||
var mopidyConstructor = Mopidy; // To trick jshint into submission
|
||||
|
||||
var mopidy = mopidyConstructor({webSocket: {}});
|
||||
var mopidy = mopidyConstructor({
|
||||
callingConvention: "by-position-or-by-name",
|
||||
webSocket: {}
|
||||
});
|
||||
|
||||
assert.isObject(mopidy);
|
||||
assert(mopidy instanceof Mopidy);
|
||||
@ -75,7 +133,10 @@ buster.testCase("Mopidy", {
|
||||
|
||||
".connect": {
|
||||
"connects when autoConnect is false": function () {
|
||||
var mopidy = new Mopidy({autoConnect: false});
|
||||
var mopidy = new Mopidy({
|
||||
autoConnect: false,
|
||||
callingConvention: "by-position-or-by-name"
|
||||
});
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
|
||||
mopidy.connect();
|
||||
@ -84,12 +145,15 @@ buster.testCase("Mopidy", {
|
||||
document.location.host || "localhost";
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + currentHost + "/mopidy/ws/");
|
||||
"ws://" + currentHost + "/mopidy/ws");
|
||||
},
|
||||
|
||||
"does nothing when the WebSocket is open": function () {
|
||||
this.webSocket.readyState = Mopidy.WebSocket.OPEN;
|
||||
var mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
var mopidy = new Mopidy({
|
||||
callingConvention: "by-position-or-by-name",
|
||||
webSocket: this.webSocket
|
||||
});
|
||||
|
||||
mopidy.connect();
|
||||
|
||||
@ -169,12 +233,18 @@ buster.testCase("Mopidy", {
|
||||
this.mopidy._cleanup(closeEvent);
|
||||
|
||||
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
|
||||
when.join(promise1, promise2).then(done(function () {
|
||||
assert(false, "Promises should be rejected");
|
||||
}), done(function (error) {
|
||||
assert.equals(error.message, "WebSocket closed");
|
||||
assert.same(error.closeEvent, closeEvent);
|
||||
}));
|
||||
when.settle([promise1, promise2]).done(
|
||||
done(function (descriptors) {
|
||||
assert.equals(descriptors.length, 2);
|
||||
descriptors.forEach(function (d) {
|
||||
assert.equals(d.state, "rejected");
|
||||
assert(d.reason instanceof Error);
|
||||
assert(d.reason instanceof Mopidy.ConnectionError);
|
||||
assert.equals(d.reason.message, "WebSocket closed");
|
||||
assert.same(d.reason.closeEvent, closeEvent);
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"emits 'state:offline' event when done": function () {
|
||||
@ -388,12 +458,17 @@ buster.testCase("Mopidy", {
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is still connecting");
|
||||
}));
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof Mopidy.ConnectionError);
|
||||
assert.equals(
|
||||
error.message, "WebSocket is still connecting");
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSING": function (done) {
|
||||
@ -402,12 +477,16 @@ buster.testCase("Mopidy", {
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is closing");
|
||||
}));
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof Mopidy.ConnectionError);
|
||||
assert.equals(error.message, "WebSocket is closing");
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSED": function (done) {
|
||||
@ -416,12 +495,16 @@ buster.testCase("Mopidy", {
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is closed");
|
||||
}));
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof Mopidy.ConnectionError);
|
||||
assert.equals(error.message, "WebSocket is closed");
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -544,7 +627,11 @@ buster.testCase("Mopidy", {
|
||||
"rejects and logs requests which get errors back": function (done) {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseError = {message: "Error", data: {}};
|
||||
var responseError = {
|
||||
code: -32601,
|
||||
message: "Method not found",
|
||||
data: {}
|
||||
};
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0],
|
||||
@ -555,11 +642,49 @@ buster.testCase("Mopidy", {
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Server returned error:", responseError);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(error, responseError);
|
||||
}));
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert.equals(error.code, responseError.code);
|
||||
assert.equals(error.message, responseError.message);
|
||||
assert.equals(error.data, responseError.data);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"rejects and logs requests which get errors without data": function (done) {
|
||||
var stub = this.stub(this.mopidy._console, "warn");
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseError = {
|
||||
code: -32601,
|
||||
message: "Method not found"
|
||||
// 'data' key intentionally missing
|
||||
};
|
||||
var responseMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: Object.keys(this.mopidy._pendingRequests)[0],
|
||||
error: responseError
|
||||
};
|
||||
|
||||
this.mopidy._handleResponse(responseMessage);
|
||||
|
||||
assert.calledOnceWith(stub,
|
||||
"Server returned error:", responseError);
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof Mopidy.ServerError);
|
||||
assert.equals(error.code, responseError.code);
|
||||
assert.equals(error.message, responseError.message);
|
||||
refute.defined(error.data);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"rejects and logs responses without result or error": function (done) {
|
||||
@ -575,14 +700,18 @@ buster.testCase("Mopidy", {
|
||||
assert.calledOnceWith(stub,
|
||||
"Response without 'result' or 'error' received. Message was:",
|
||||
responseMessage);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message,
|
||||
"Response without 'result' or 'error' received");
|
||||
assert.equals(error.data.response, responseMessage);
|
||||
}));
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert.equals(
|
||||
error.message,
|
||||
"Response without 'result' or 'error' received");
|
||||
assert.equals(error.data.response, responseMessage);
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -699,6 +828,137 @@ buster.testCase("Mopidy", {
|
||||
this.mopidy._createApi({});
|
||||
|
||||
assert.calledOnceWith(spy);
|
||||
},
|
||||
|
||||
"by-position-only calling convention": {
|
||||
setUp: function () {
|
||||
this.mopidy = new Mopidy({
|
||||
webSocket: this.webSocket,
|
||||
callingConvention: "by-position-only"
|
||||
});
|
||||
this.mopidy._createApi({
|
||||
foo: {
|
||||
params: ["bar", "baz"]
|
||||
}
|
||||
});
|
||||
this.sendStub = this.stub(this.mopidy, "_send");
|
||||
|
||||
},
|
||||
|
||||
"sends no params if no arguments passed to function": function () {
|
||||
this.mopidy.foo();
|
||||
|
||||
assert.calledOnceWith(this.sendStub, {method: "foo"});
|
||||
},
|
||||
|
||||
"sends messages with function arguments unchanged": function () {
|
||||
this.mopidy.foo(31, 97);
|
||||
|
||||
assert.calledOnceWith(this.sendStub, {
|
||||
method: "foo",
|
||||
params: [31, 97]
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
"by-position-or-by-name calling convention": {
|
||||
setUp: function () {
|
||||
this.mopidy = new Mopidy({
|
||||
webSocket: this.webSocket,
|
||||
callingConvention: "by-position-or-by-name"
|
||||
});
|
||||
this.mopidy._createApi({
|
||||
foo: {
|
||||
params: ["bar", "baz"]
|
||||
}
|
||||
});
|
||||
this.sendStub = this.stub(this.mopidy, "_send");
|
||||
},
|
||||
|
||||
"must be turned on manually": function () {
|
||||
assert.equals(
|
||||
this.mopidy._settings.callingConvention,
|
||||
"by-position-or-by-name");
|
||||
},
|
||||
|
||||
"sends no params if no arguments passed to function": function () {
|
||||
this.mopidy.foo();
|
||||
|
||||
assert.calledOnceWith(this.sendStub, {method: "foo"});
|
||||
},
|
||||
|
||||
"sends by-position if argument is a list": function () {
|
||||
this.mopidy.foo([31, 97]);
|
||||
|
||||
assert.calledOnceWith(this.sendStub, {
|
||||
method: "foo",
|
||||
params: [31, 97]
|
||||
});
|
||||
},
|
||||
|
||||
"sends by-name if argument is an object": function () {
|
||||
this.mopidy.foo({bar: 31, baz: 97});
|
||||
|
||||
assert.calledOnceWith(this.sendStub, {
|
||||
method: "foo",
|
||||
params: {bar: 31, baz: 97}
|
||||
});
|
||||
},
|
||||
|
||||
"rejects with error if more than one argument": function (done) {
|
||||
var promise = this.mopidy.foo([1, 2], {c: 3, d: 4});
|
||||
|
||||
refute.called(this.sendStub);
|
||||
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert.equals(
|
||||
error.message,
|
||||
"Expected zero arguments, a single array, " +
|
||||
"or a single object.");
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"rejects with error if string": function (done) {
|
||||
var promise = this.mopidy.foo("hello");
|
||||
|
||||
refute.called(this.sendStub);
|
||||
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof TypeError);
|
||||
assert.equals(
|
||||
error.message, "Expected an array or an object.");
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
"rejects with error if number": function (done) {
|
||||
var promise = this.mopidy.foo(1337);
|
||||
|
||||
refute.called(this.sendStub);
|
||||
|
||||
promise.done(
|
||||
done(function () {
|
||||
assert(false);
|
||||
}),
|
||||
done(function (error) {
|
||||
assert(error instanceof Error);
|
||||
assert(error instanceof TypeError);
|
||||
assert.equals(
|
||||
error.message, "Expected an array or an object.");
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from distutils.version import StrictVersion as SV
|
||||
import sys
|
||||
import warnings
|
||||
from distutils.version import StrictVersion as SV
|
||||
|
||||
import pykka
|
||||
|
||||
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.18.3'
|
||||
__version__ = '0.19.0'
|
||||
|
||||
@ -25,9 +25,8 @@ mopidy_args = sys.argv[1:]
|
||||
sys.argv[1:] = []
|
||||
|
||||
|
||||
from mopidy import commands, ext
|
||||
from mopidy import config as config_lib
|
||||
from mopidy.utils import log, path, process, versioning
|
||||
from mopidy import commands, config as config_lib, ext
|
||||
from mopidy.utils import encoding, log, path, process, versioning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -150,9 +149,10 @@ def create_file_structures_and_config(args, extensions):
|
||||
default = config_lib.format_initial(extensions)
|
||||
path.get_or_create_file(config_file, mkdir=False, content=default)
|
||||
logger.info('Initialized %s with default config', config_file)
|
||||
except IOError as e:
|
||||
logger.warning('Unable to initialize %s with default config: %s',
|
||||
config_file, e)
|
||||
except IOError as error:
|
||||
logger.warning(
|
||||
'Unable to initialize %s with default config: %s',
|
||||
config_file, encoding.locale_decode(error))
|
||||
|
||||
|
||||
def check_old_locations():
|
||||
|
||||
@ -5,5 +5,6 @@ from .actor import Audio
|
||||
from .dummy import DummyAudio
|
||||
from .listener import AudioListener
|
||||
from .constants import PlaybackState
|
||||
from .utils import (calculate_duration, create_buffer, millisecond_to_clocktime,
|
||||
supported_uri_schemes)
|
||||
from .utils import (
|
||||
calculate_duration, create_buffer, millisecond_to_clocktime,
|
||||
supported_uri_schemes)
|
||||
|
||||
@ -1,27 +1,30 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
import logging
|
||||
|
||||
import gobject
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst # noqa
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.audio import playlists, utils
|
||||
from mopidy.audio.constants import PlaybackState
|
||||
from mopidy.audio.listener import AudioListener
|
||||
from mopidy.utils import process
|
||||
|
||||
from . import mixers, playlists, utils
|
||||
from .constants import PlaybackState
|
||||
from .listener import AudioListener
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mixers.register_mixers()
|
||||
|
||||
playlists.register_typefinders()
|
||||
playlists.register_elements()
|
||||
|
||||
_GST_STATE_MAPPING = {
|
||||
gst.STATE_PLAYING: PlaybackState.PLAYING,
|
||||
gst.STATE_PAUSED: PlaybackState.PAUSED,
|
||||
gst.STATE_NULL: PlaybackState.STOPPED}
|
||||
|
||||
MB = 1 << 20
|
||||
|
||||
@ -50,20 +53,17 @@ class Audio(pykka.ThreadingActor):
|
||||
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
|
||||
state = PlaybackState.STOPPED
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, mixer):
|
||||
super(Audio, self).__init__()
|
||||
|
||||
self._config = config
|
||||
self._mixer = mixer
|
||||
self._target_state = gst.STATE_NULL
|
||||
self._buffering = False
|
||||
|
||||
self._playbin = None
|
||||
self._signal_ids = {} # {(element, event): signal_id}
|
||||
|
||||
self._mixer = None
|
||||
self._mixer_track = None
|
||||
self._mixer_scale = None
|
||||
self._software_mixing = False
|
||||
self._volume_set = None
|
||||
|
||||
self._appsrc = None
|
||||
self._appsrc_caps = None
|
||||
self._appsrc_need_data_callback = None
|
||||
@ -74,8 +74,8 @@ class Audio(pykka.ThreadingActor):
|
||||
try:
|
||||
self._setup_playbin()
|
||||
self._setup_output()
|
||||
self._setup_visualizer()
|
||||
self._setup_mixer()
|
||||
self._setup_visualizer()
|
||||
self._setup_message_processor()
|
||||
except gobject.GError as ex:
|
||||
logger.exception(ex)
|
||||
@ -100,8 +100,12 @@ class Audio(pykka.ThreadingActor):
|
||||
playbin = gst.element_factory_make('playbin2')
|
||||
playbin.set_property('flags', PLAYBIN_FLAGS)
|
||||
|
||||
playbin.set_property('buffer-size', 2*1024*1024)
|
||||
playbin.set_property('buffer-duration', 2*gst.SECOND)
|
||||
|
||||
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
|
||||
self._connect(playbin, 'notify::source', self._on_new_source)
|
||||
self._connect(playbin, 'source-setup', self._on_source_setup)
|
||||
|
||||
self._playbin = playbin
|
||||
|
||||
@ -133,6 +137,22 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
self._appsrc = source
|
||||
|
||||
def _on_source_setup(self, element, source):
|
||||
scheme = 'http'
|
||||
hostname = self._config['proxy']['hostname']
|
||||
port = 80
|
||||
|
||||
if hasattr(source.props, 'proxy') and hostname:
|
||||
if self._config['proxy']['port']:
|
||||
port = self._config['proxy']['port']
|
||||
if self._config['proxy']['scheme']:
|
||||
scheme = self._config['proxy']['scheme']
|
||||
|
||||
proxy = "%s://%s:%d" % (scheme, hostname, port)
|
||||
source.set_property('proxy', proxy)
|
||||
source.set_property('proxy-id', self._config['proxy']['username'])
|
||||
source.set_property('proxy-pw', self._config['proxy']['password'])
|
||||
|
||||
def _appsrc_on_need_data(self, appsrc, gst_length_hint):
|
||||
length_hint = utils.clocktime_to_millisecond(gst_length_hint)
|
||||
if self._appsrc_need_data_callback is not None:
|
||||
@ -153,6 +173,7 @@ class Audio(pykka.ThreadingActor):
|
||||
def _teardown_playbin(self):
|
||||
self._disconnect(self._playbin, 'about-to-finish')
|
||||
self._disconnect(self._playbin, 'notify::source')
|
||||
self._disconnect(self._playbin, 'source-setup')
|
||||
self._playbin.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_output(self):
|
||||
@ -167,6 +188,23 @@ class Audio(pykka.ThreadingActor):
|
||||
'Failed to create audio output "%s": %s', output_desc, ex)
|
||||
process.exit_process()
|
||||
|
||||
def _setup_mixer(self):
|
||||
if self._config['audio']['mixer'] != 'software':
|
||||
return
|
||||
self._mixer.audio = self.actor_ref.proxy()
|
||||
self._connect(self._playbin, 'notify::volume', self._on_mixer_change)
|
||||
self._connect(self._playbin, 'notify::mute', self._on_mixer_change)
|
||||
|
||||
def _on_mixer_change(self, element, gparamspec):
|
||||
self._mixer.trigger_events_for_changed_values()
|
||||
|
||||
def _teardown_mixer(self):
|
||||
if self._config['audio']['mixer'] != 'software':
|
||||
return
|
||||
self._disconnect(self._playbin, 'notify::volume')
|
||||
self._disconnect(self._playbin, 'notify::mute')
|
||||
self._mixer.audio = None
|
||||
|
||||
def _setup_visualizer(self):
|
||||
visualizer_element = self._config['audio']['visualizer']
|
||||
if not visualizer_element:
|
||||
@ -181,86 +219,6 @@ class Audio(pykka.ThreadingActor):
|
||||
'Failed to create audio visualizer "%s": %s',
|
||||
visualizer_element, ex)
|
||||
|
||||
def _setup_mixer(self):
|
||||
mixer_desc = self._config['audio']['mixer']
|
||||
track_desc = self._config['audio']['mixer_track']
|
||||
volume = self._config['audio']['mixer_volume']
|
||||
|
||||
if mixer_desc is None:
|
||||
logger.info('Not setting up audio mixer')
|
||||
return
|
||||
|
||||
if mixer_desc == 'software':
|
||||
self._software_mixing = True
|
||||
logger.info('Audio mixer is using software mixing')
|
||||
if volume is not None:
|
||||
self.set_volume(volume)
|
||||
logger.info('Audio mixer volume set to %d', volume)
|
||||
return
|
||||
|
||||
try:
|
||||
mixerbin = gst.parse_bin_from_description(
|
||||
mixer_desc, ghost_unconnected_pads=False)
|
||||
except gobject.GError as ex:
|
||||
logger.warning(
|
||||
'Failed to create audio mixer "%s": %s', mixer_desc, ex)
|
||||
return
|
||||
|
||||
# We assume that the bin will contain a single mixer.
|
||||
mixer = mixerbin.get_by_interface(b'GstMixer')
|
||||
if not mixer:
|
||||
logger.warning(
|
||||
'Did not find any audio mixers in "%s"', mixer_desc)
|
||||
return
|
||||
|
||||
if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
|
||||
logger.warning(
|
||||
'Setting audio mixer "%s" to READY failed', mixer_desc)
|
||||
return
|
||||
|
||||
track = self._select_mixer_track(mixer, track_desc)
|
||||
if not track:
|
||||
logger.warning('Could not find usable audio mixer track')
|
||||
return
|
||||
|
||||
self._mixer = mixer
|
||||
self._mixer_track = track
|
||||
self._mixer_scale = (
|
||||
self._mixer_track.min_volume, self._mixer_track.max_volume)
|
||||
|
||||
logger.info(
|
||||
'Audio mixer set to "%s" using track "%s"',
|
||||
str(mixer.get_factory().get_name()).decode('utf-8'),
|
||||
str(track.label).decode('utf-8'))
|
||||
|
||||
if volume is not None:
|
||||
self.set_volume(volume)
|
||||
logger.info('Audio mixer volume set to %d', volume)
|
||||
|
||||
def _select_mixer_track(self, mixer, track_label):
|
||||
# Ignore tracks without volumes, then look for track with
|
||||
# label equal to the audio/mixer_track config value, otherwise fallback
|
||||
# to first usable track hoping the mixer gave them to us in a sensible
|
||||
# order.
|
||||
|
||||
usable_tracks = []
|
||||
for track in mixer.list_tracks():
|
||||
if not mixer.get_volume(track):
|
||||
continue
|
||||
|
||||
if track_label and track.label == track_label:
|
||||
return track
|
||||
elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT):
|
||||
usable_tracks.append(track)
|
||||
|
||||
if usable_tracks:
|
||||
return usable_tracks[0]
|
||||
|
||||
def _teardown_mixer(self):
|
||||
if self._mixer is not None:
|
||||
self._mixer.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_message_processor(self):
|
||||
bus = self._playbin.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@ -271,27 +229,17 @@ class Audio(pykka.ThreadingActor):
|
||||
self._disconnect(bus, 'message')
|
||||
bus.remove_signal_watch()
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
if (message.type == gst.MESSAGE_STATE_CHANGED
|
||||
and message.src == self._playbin):
|
||||
old_state, new_state, pending_state = message.parse_state_changed()
|
||||
self._on_playbin_state_changed(old_state, new_state, pending_state)
|
||||
elif message.type == gst.MESSAGE_BUFFERING:
|
||||
percent = message.parse_buffering()
|
||||
logger.debug('Buffer %d%% full', percent)
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
def _on_message(self, bus, msg):
|
||||
if msg.type == gst.MESSAGE_STATE_CHANGED and msg.src == self._playbin:
|
||||
self._on_playbin_state_changed(*msg.parse_state_changed())
|
||||
elif msg.type == gst.MESSAGE_BUFFERING:
|
||||
self._on_buffering(msg.parse_buffering())
|
||||
elif msg.type == gst.MESSAGE_EOS:
|
||||
self._on_end_of_stream()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
error, debug = message.parse_error()
|
||||
logger.error(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
self.stop_playback()
|
||||
elif message.type == gst.MESSAGE_WARNING:
|
||||
error, debug = message.parse_warning()
|
||||
logger.warning(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
elif msg.type == gst.MESSAGE_ERROR:
|
||||
self._on_error(*msg.parse_error())
|
||||
elif msg.type == gst.MESSAGE_WARNING:
|
||||
self._on_warning(*msg.parse_warning())
|
||||
|
||||
def _on_playbin_state_changed(self, old_state, new_state, pending_state):
|
||||
if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
|
||||
@ -307,25 +255,45 @@ class Audio(pykka.ThreadingActor):
|
||||
if new_state == gst.STATE_READY:
|
||||
return # Ignore READY state as it's GStreamer specific
|
||||
|
||||
if new_state == gst.STATE_PLAYING:
|
||||
new_state = PlaybackState.PLAYING
|
||||
elif new_state == gst.STATE_PAUSED:
|
||||
new_state = PlaybackState.PAUSED
|
||||
elif new_state == gst.STATE_NULL:
|
||||
new_state = PlaybackState.STOPPED
|
||||
|
||||
new_state = _GST_STATE_MAPPING[new_state]
|
||||
old_state, self.state = self.state, new_state
|
||||
|
||||
target_state = _GST_STATE_MAPPING[self._target_state]
|
||||
if target_state == new_state:
|
||||
target_state = None
|
||||
|
||||
logger.debug(
|
||||
'Triggering event: state_changed(old_state=%s, new_state=%s)',
|
||||
old_state, new_state)
|
||||
AudioListener.send(
|
||||
'state_changed', old_state=old_state, new_state=new_state)
|
||||
'Triggering event: state_changed(old_state=%s, new_state=%s, '
|
||||
'target_state=%s)', old_state, new_state, target_state)
|
||||
AudioListener.send('state_changed', old_state=old_state,
|
||||
new_state=new_state, target_state=target_state)
|
||||
|
||||
def _on_buffering(self, percent):
|
||||
if percent < 10 and not self._buffering:
|
||||
self._playbin.set_state(gst.STATE_PAUSED)
|
||||
self._buffering = True
|
||||
if percent == 100:
|
||||
self._buffering = False
|
||||
if self._target_state == gst.STATE_PLAYING:
|
||||
self._playbin.set_state(gst.STATE_PLAYING)
|
||||
|
||||
logger.debug('Buffer %d%% full', percent)
|
||||
|
||||
def _on_end_of_stream(self):
|
||||
logger.debug('Triggering reached_end_of_stream event')
|
||||
AudioListener.send('reached_end_of_stream')
|
||||
|
||||
def _on_error(self, error, debug):
|
||||
logger.error(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
self.stop_playback()
|
||||
|
||||
def _on_warning(self, error, debug):
|
||||
logger.warning(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
|
||||
def set_uri(self, uri):
|
||||
"""
|
||||
Set URI of audio to be played.
|
||||
@ -447,6 +415,7 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
self._buffering = False
|
||||
return self._set_state(gst.STATE_NULL)
|
||||
|
||||
def _set_state(self, state):
|
||||
@ -470,6 +439,7 @@ class Audio(pykka.ThreadingActor):
|
||||
:type state: :class:`gst.State`
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
self._target_state = state
|
||||
result = self._playbin.set_state(state)
|
||||
if result == gst.STATE_CHANGE_FAILURE:
|
||||
logger.warning(
|
||||
@ -486,108 +456,49 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Get volume level of the installed mixer.
|
||||
Get volume level of the software mixer.
|
||||
|
||||
Example values:
|
||||
|
||||
0:
|
||||
Muted.
|
||||
Minimum volume.
|
||||
100:
|
||||
Max volume for given system.
|
||||
:class:`None`:
|
||||
No mixer present, so the volume is unknown.
|
||||
Maximum volume.
|
||||
|
||||
:rtype: int in range [0..100] or :class:`None`
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
if self._software_mixing:
|
||||
return int(round(self._playbin.get_property('volume') * 100))
|
||||
|
||||
if self._mixer is None:
|
||||
return None
|
||||
|
||||
volumes = self._mixer.get_volume(self._mixer_track)
|
||||
avg_volume = float(sum(volumes)) / len(volumes)
|
||||
|
||||
internal_scale = (0, 100)
|
||||
|
||||
if self._volume_set is not None:
|
||||
volume_set_on_mixer_scale = self._rescale(
|
||||
self._volume_set, old=internal_scale, new=self._mixer_scale)
|
||||
else:
|
||||
volume_set_on_mixer_scale = None
|
||||
|
||||
if volume_set_on_mixer_scale == avg_volume:
|
||||
return self._volume_set
|
||||
else:
|
||||
return self._rescale(
|
||||
avg_volume, old=self._mixer_scale, new=internal_scale)
|
||||
return int(round(self._playbin.get_property('volume') * 100))
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set volume level of the installed mixer.
|
||||
Set volume level of the software mixer.
|
||||
|
||||
:param volume: the volume in the range [0..100]
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
if self._software_mixing:
|
||||
self._playbin.set_property('volume', volume / 100.0)
|
||||
return True
|
||||
|
||||
if self._mixer is None:
|
||||
return False
|
||||
|
||||
self._volume_set = volume
|
||||
|
||||
internal_scale = (0, 100)
|
||||
|
||||
volume = self._rescale(
|
||||
volume, old=internal_scale, new=self._mixer_scale)
|
||||
|
||||
volumes = (volume,) * self._mixer_track.num_channels
|
||||
self._mixer.set_volume(self._mixer_track, volumes)
|
||||
|
||||
return self._mixer.get_volume(self._mixer_track) == volumes
|
||||
|
||||
def _rescale(self, value, old=None, new=None):
|
||||
"""Convert value between scales."""
|
||||
new_min, new_max = new
|
||||
old_min, old_max = old
|
||||
if old_min == old_max:
|
||||
return old_max
|
||||
scaling = float(new_max - new_min) / (old_max - old_min)
|
||||
return int(round(scaling * (value - old_min) + new_min))
|
||||
self._playbin.set_property('volume', volume / 100.0)
|
||||
return True
|
||||
|
||||
def get_mute(self):
|
||||
"""
|
||||
Get mute status of the installed mixer.
|
||||
Get mute status of the software mixer.
|
||||
|
||||
:rtype: :class:`True` if muted, :class:`False` if unmuted,
|
||||
:class:`None` if no mixer is installed.
|
||||
"""
|
||||
if self._software_mixing:
|
||||
return self._playbin.get_property('mute')
|
||||
|
||||
if self._mixer_track is None:
|
||||
return None
|
||||
|
||||
return bool(self._mixer_track.flags & gst.interfaces.MIXER_TRACK_MUTE)
|
||||
return self._playbin.get_property('mute')
|
||||
|
||||
def set_mute(self, mute):
|
||||
"""
|
||||
Mute or unmute of the installed mixer.
|
||||
Mute or unmute of the software mixer.
|
||||
|
||||
:param mute: Wether to mute the mixer or not.
|
||||
:param mute: Whether to mute the mixer or not.
|
||||
:type mute: bool
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
if self._software_mixing:
|
||||
return self._playbin.set_property('mute', bool(mute))
|
||||
|
||||
if self._mixer_track is None:
|
||||
return False
|
||||
|
||||
return self._mixer.set_mute(self._mixer_track, bool(mute))
|
||||
self._playbin.set_property('mute', bool(mute))
|
||||
return True
|
||||
|
||||
def set_metadata(self, track):
|
||||
"""
|
||||
|
||||
@ -27,17 +27,31 @@ class AudioListener(listener.Listener):
|
||||
"""
|
||||
pass
|
||||
|
||||
def state_changed(self, old_state, new_state):
|
||||
def state_changed(self, old_state, new_state, target_state):
|
||||
"""
|
||||
Called after the playback state have changed.
|
||||
|
||||
Will be called for both immediate and async state changes in GStreamer.
|
||||
|
||||
Target state is used to when we should be in the target state, but
|
||||
temporarily need to switch to an other state. A typical example of this
|
||||
is buffering. When this happens an event with
|
||||
`old=PLAYING, new=PAUSED, target=PLAYING` will be emitted. Once we have
|
||||
caught up a `old=PAUSED, new=PLAYING, target=None` event will be
|
||||
be generated.
|
||||
|
||||
Regular state changes will not have target state set as they are final
|
||||
states which should be stable.
|
||||
|
||||
*MAY* be implemented by actor.
|
||||
|
||||
:param old_state: the state before the change
|
||||
:type old_state: string from :class:`mopidy.core.PlaybackState` field
|
||||
:param new_state: the state after the change
|
||||
:type new_state: A :class:`mopidy.core.PlaybackState` field
|
||||
:type new_state: string from :class:`mopidy.core.PlaybackState` field
|
||||
:param target_state: the intended state
|
||||
:type target_state: string from :class:`mopidy.core.PlaybackState`
|
||||
field or :class:`None` if this is a final state.
|
||||
"""
|
||||
pass
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
from .auto import AutoAudioMixer
|
||||
from .fake import FakeMixer
|
||||
|
||||
|
||||
def register_mixer(mixer_class):
|
||||
gobject.type_register(mixer_class)
|
||||
gst.element_register(
|
||||
mixer_class, mixer_class.__name__.lower(), gst.RANK_MARGINAL)
|
||||
|
||||
|
||||
def register_mixers():
|
||||
register_mixer(AutoAudioMixer)
|
||||
register_mixer(FakeMixer)
|
||||
@ -1,76 +0,0 @@
|
||||
"""Mixer element that automatically selects the real mixer to use.
|
||||
|
||||
Set the :confval:`audio/mixer` config value to ``autoaudiomixer`` to use this
|
||||
mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: we might want to add some ranking to the mixers we know about?
|
||||
class AutoAudioMixer(gst.Bin):
|
||||
__gstdetails__ = (
|
||||
'AutoAudioMixer',
|
||||
'Mixer',
|
||||
'Element automatically selects a mixer.',
|
||||
'Mopidy')
|
||||
|
||||
def __init__(self):
|
||||
gst.Bin.__init__(self)
|
||||
mixer = self._find_mixer()
|
||||
if mixer:
|
||||
self.add(mixer)
|
||||
logger.debug('AutoAudioMixer chose: %s', mixer.get_name())
|
||||
else:
|
||||
logger.debug('AutoAudioMixer did not find any usable mixers')
|
||||
|
||||
def _find_mixer(self):
|
||||
registry = gst.registry_get_default()
|
||||
|
||||
factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY)
|
||||
factories.sort(key=lambda f: (-f.get_rank(), f.get_name()))
|
||||
|
||||
for factory in factories:
|
||||
# Avoid sink/srcs that implement mixing.
|
||||
if factory.get_klass() != 'Generic/Audio':
|
||||
continue
|
||||
# Avoid anything that doesn't implement mixing.
|
||||
elif not factory.has_interface('GstMixer'):
|
||||
continue
|
||||
|
||||
if self._test_mixer(factory):
|
||||
return factory.create()
|
||||
|
||||
return None
|
||||
|
||||
def _test_mixer(self, factory):
|
||||
element = factory.create()
|
||||
if not element:
|
||||
return False
|
||||
|
||||
try:
|
||||
result = element.set_state(gst.STATE_READY)
|
||||
if result != gst.STATE_CHANGE_SUCCESS:
|
||||
return False
|
||||
|
||||
# Trust that the default device is sane and just check tracks.
|
||||
return self._test_tracks(element)
|
||||
finally:
|
||||
element.set_state(gst.STATE_NULL)
|
||||
|
||||
def _test_tracks(self, element):
|
||||
# Only allow elements that have a least one output track.
|
||||
flags = gst.interfaces.MIXER_TRACK_OUTPUT
|
||||
|
||||
for track in element.list_tracks():
|
||||
if track.flags & flags:
|
||||
return True
|
||||
return False
|
||||
@ -1,49 +0,0 @@
|
||||
"""Fake mixer for use in tests.
|
||||
|
||||
Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this
|
||||
mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gobject
|
||||
import gst
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
|
||||
__gstdetails__ = (
|
||||
'FakeMixer',
|
||||
'Mixer',
|
||||
'Fake mixer for use in tests.',
|
||||
'Mopidy')
|
||||
|
||||
track_label = gobject.property(type=str, default='Master')
|
||||
track_initial_volume = gobject.property(type=int, default=0)
|
||||
track_min_volume = gobject.property(type=int, default=0)
|
||||
track_max_volume = gobject.property(type=int, default=100)
|
||||
track_num_channels = gobject.property(type=int, default=2)
|
||||
track_flags = gobject.property(type=int, default=(
|
||||
gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT))
|
||||
|
||||
def list_tracks(self):
|
||||
track = utils.create_track(
|
||||
self.track_label,
|
||||
self.track_initial_volume,
|
||||
self.track_min_volume,
|
||||
self.track_max_volume,
|
||||
self.track_num_channels,
|
||||
self.track_flags)
|
||||
return [track]
|
||||
|
||||
def get_volume(self, track):
|
||||
return track.volumes
|
||||
|
||||
def set_volume(self, track, volumes):
|
||||
track.volumes = volumes
|
||||
|
||||
def set_record(self, track, record):
|
||||
pass
|
||||
@ -1,37 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
|
||||
def create_track(label, initial_volume, min_volume, max_volume,
|
||||
num_channels, flags):
|
||||
|
||||
class Track(gst.interfaces.MixerTrack):
|
||||
def __init__(self):
|
||||
super(Track, self).__init__()
|
||||
self.volumes = (initial_volume,) * self.num_channels
|
||||
|
||||
@gobject.property
|
||||
def label(self):
|
||||
return label
|
||||
|
||||
@gobject.property
|
||||
def min_volume(self):
|
||||
return min_volume
|
||||
|
||||
@gobject.property
|
||||
def max_volume(self):
|
||||
return max_volume
|
||||
|
||||
@gobject.property
|
||||
def num_channels(self):
|
||||
return num_channels
|
||||
|
||||
@gobject.property
|
||||
def flags(self):
|
||||
return flags
|
||||
|
||||
return Track()
|
||||
@ -1,13 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
import ConfigParser as configparser
|
||||
import io
|
||||
|
||||
import gobject
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst # noqa
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as elementtree
|
||||
except ImportError:
|
||||
@ -17,16 +18,16 @@ except ImportError:
|
||||
# TODO: make detect_FOO_header reusable in general mopidy code.
|
||||
# i.e. give it just a "peek" like function.
|
||||
def detect_m3u_header(typefind):
|
||||
return typefind.peek(0, 8) == b'#EXTM3U\n'
|
||||
return typefind.peek(0, 7).upper() == b'#EXTM3U'
|
||||
|
||||
|
||||
def detect_pls_header(typefind):
|
||||
return typefind.peek(0, 11).lower() == b'[playlist]\n'
|
||||
return typefind.peek(0, 10).lower() == b'[playlist]'
|
||||
|
||||
|
||||
def detect_xspf_header(typefind):
|
||||
data = typefind.peek(0, 150)
|
||||
if b'xspf' not in data:
|
||||
if b'xspf' not in data.lower():
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -40,7 +41,7 @@ def detect_xspf_header(typefind):
|
||||
|
||||
def detect_asx_header(typefind):
|
||||
data = typefind.peek(0, 50)
|
||||
if b'asx' not in data:
|
||||
if b'asx' not in data.lower():
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -81,6 +82,7 @@ def parse_pls(data):
|
||||
|
||||
def parse_xspf(data):
|
||||
try:
|
||||
# Last element will be root.
|
||||
for event, element in elementtree.iterparse(data):
|
||||
element.tag = element.tag.lower() # normalize
|
||||
except elementtree.ParseError:
|
||||
@ -93,14 +95,18 @@ def parse_xspf(data):
|
||||
|
||||
def parse_asx(data):
|
||||
try:
|
||||
# Last element will be root.
|
||||
for event, element in elementtree.iterparse(data):
|
||||
element.tag = element.tag.lower() # normalize
|
||||
except elementtree.ParseError:
|
||||
return
|
||||
|
||||
for ref in element.findall('entry/ref'):
|
||||
for ref in element.findall('entry/ref[@href]'):
|
||||
yield ref.get('href', '').strip()
|
||||
|
||||
for entry in element.findall('entry[@href]'):
|
||||
yield entry.get('href', '').strip()
|
||||
|
||||
|
||||
def parse_urilist(data):
|
||||
for line in data.readlines():
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst # noqa
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy.models import Track, Artist, Album
|
||||
from mopidy.utils import path
|
||||
from mopidy.models import Album, Artist, Track
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
|
||||
class Scanner(object):
|
||||
@ -90,7 +90,8 @@ class Scanner(object):
|
||||
message = self._bus.pop()
|
||||
|
||||
if message.type == gst.MESSAGE_ERROR:
|
||||
raise exceptions.ScannerError(message.parse_error()[0])
|
||||
raise exceptions.ScannerError(
|
||||
encoding.locale_decode(message.parse_error()[0]))
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
return tags
|
||||
elif message.type == gst.MESSAGE_ASYNC_DONE:
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gst # noqa
|
||||
|
||||
|
||||
def calculate_duration(num_samples, sample_rate):
|
||||
|
||||
@ -6,6 +6,19 @@ from mopidy import listener
|
||||
|
||||
|
||||
class Backend(object):
|
||||
"""Backend API
|
||||
|
||||
If the backend has problems during initialization it should raise
|
||||
:exc:`mopidy.exceptions.BackendError` with a descriptive error message.
|
||||
This will make Mopidy print the error message and exit so that the user can
|
||||
fix the issue.
|
||||
|
||||
:param config: the entire Mopidy configuration
|
||||
:type config: dict
|
||||
:param audio: actor proxy for the audio subsystem
|
||||
:type audio: :class:`pykka.ActorProxy` for :class:`mopidy.audio.Audio`
|
||||
"""
|
||||
|
||||
#: Actor proxy to an instance of :class:`mopidy.audio.Audio`.
|
||||
#:
|
||||
#: Should be passed to the backend constructor as the kwarg ``audio``,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -1,17 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.backend import (
|
||||
Backend,
|
||||
LibraryProvider as BaseLibraryProvider,
|
||||
PlaybackProvider as BasePlaybackProvider,
|
||||
PlaylistsProvider as BasePlaylistsProvider)
|
||||
|
||||
|
||||
# Make classes previously residing here available in the old location for
|
||||
# backwards compatibility with extensions targeting Mopidy < 0.18.
|
||||
__all__ = [
|
||||
'Backend',
|
||||
'BaseLibraryProvider',
|
||||
'BasePlaybackProvider',
|
||||
'BasePlaylistsProvider',
|
||||
]
|
||||
@ -1,5 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# Make classes previously residing here available in the old location for
|
||||
# backwards compatibility with extensions targeting Mopidy < 0.18.
|
||||
from mopidy.backend.dummy import * # noqa
|
||||
@ -1,8 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.backend import BackendListener
|
||||
|
||||
|
||||
# Make classes previously residing here available in the old location for
|
||||
# backwards compatibility with extensions targeting Mopidy < 0.18.
|
||||
__all__ = ['BackendListener']
|
||||
@ -7,9 +7,10 @@ import os
|
||||
import sys
|
||||
|
||||
import glib
|
||||
|
||||
import gobject
|
||||
|
||||
from mopidy import config as config_lib
|
||||
from mopidy import config as config_lib, exceptions
|
||||
from mopidy.audio import Audio
|
||||
from mopidy.core import Core
|
||||
from mopidy.utils import deps, process, versioning
|
||||
@ -99,7 +100,7 @@ class Command(object):
|
||||
self._children[name] = command
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
"""Add am argument to the parser.
|
||||
"""Add an argument to the parser.
|
||||
|
||||
This method takes all the same arguments as the
|
||||
:class:`argparse.ArgumentParser` version of this method.
|
||||
@ -260,29 +261,70 @@ class RootCommand(Command):
|
||||
def run(self, args, config):
|
||||
loop = gobject.MainLoop()
|
||||
|
||||
mixer_class = self.get_mixer_class(config, args.registry['mixer'])
|
||||
backend_classes = args.registry['backend']
|
||||
frontend_classes = args.registry['frontend']
|
||||
|
||||
try:
|
||||
audio = self.start_audio(config)
|
||||
mixer = self.start_mixer(config, mixer_class)
|
||||
audio = self.start_audio(config, mixer)
|
||||
backends = self.start_backends(config, backend_classes, audio)
|
||||
core = self.start_core(audio, backends)
|
||||
core = self.start_core(mixer, backends)
|
||||
self.start_frontends(config, frontend_classes, core)
|
||||
loop.run()
|
||||
except (exceptions.BackendError,
|
||||
exceptions.FrontendError,
|
||||
exceptions.MixerError):
|
||||
logger.info('Initialization error. Exiting...')
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Interrupted. Exiting...')
|
||||
return
|
||||
finally:
|
||||
loop.quit()
|
||||
self.stop_frontends(frontend_classes)
|
||||
self.stop_core()
|
||||
self.stop_backends(backend_classes)
|
||||
self.stop_audio()
|
||||
self.stop_mixer(mixer_class)
|
||||
process.stop_remaining_actors()
|
||||
|
||||
def start_audio(self, config):
|
||||
def get_mixer_class(self, config, mixer_classes):
|
||||
logger.debug(
|
||||
'Available Mopidy mixers: %s',
|
||||
', '.join(m.__name__ for m in mixer_classes) or 'none')
|
||||
|
||||
selected_mixers = [
|
||||
m for m in mixer_classes if m.name == config['audio']['mixer']]
|
||||
if len(selected_mixers) != 1:
|
||||
logger.error(
|
||||
'Did not find unique mixer "%s". Alternatives are: %s',
|
||||
config['audio']['mixer'],
|
||||
', '.join([m.name for m in mixer_classes]))
|
||||
process.exit_process()
|
||||
return selected_mixers[0]
|
||||
|
||||
def start_mixer(self, config, mixer_class):
|
||||
try:
|
||||
logger.info('Starting Mopidy mixer: %s', mixer_class.__name__)
|
||||
mixer = mixer_class.start(config=config).proxy()
|
||||
self.configure_mixer(config, mixer)
|
||||
return mixer
|
||||
except exceptions.MixerError as exc:
|
||||
logger.error(
|
||||
'Mixer (%s) initialization error: %s',
|
||||
mixer_class.__name__, exc.message)
|
||||
raise
|
||||
|
||||
def configure_mixer(self, config, mixer):
|
||||
volume = config['audio']['mixer_volume']
|
||||
if volume is not None:
|
||||
mixer.set_volume(volume)
|
||||
logger.info('Mixer volume set to %d', volume)
|
||||
else:
|
||||
logger.debug('Mixer volume left unchanged')
|
||||
|
||||
def start_audio(self, config, mixer):
|
||||
logger.info('Starting Mopidy audio')
|
||||
return Audio.start(config=config).proxy()
|
||||
return Audio.start(config=config, mixer=mixer).proxy()
|
||||
|
||||
def start_backends(self, config, backend_classes, audio):
|
||||
logger.info(
|
||||
@ -291,14 +333,21 @@ class RootCommand(Command):
|
||||
|
||||
backends = []
|
||||
for backend_class in backend_classes:
|
||||
backend = backend_class.start(config=config, audio=audio).proxy()
|
||||
backends.append(backend)
|
||||
try:
|
||||
backend = backend_class.start(
|
||||
config=config, audio=audio).proxy()
|
||||
backends.append(backend)
|
||||
except exceptions.BackendError as exc:
|
||||
logger.error(
|
||||
'Backend (%s) initialization error: %s',
|
||||
backend_class.__name__, exc.message)
|
||||
raise
|
||||
|
||||
return backends
|
||||
|
||||
def start_core(self, audio, backends):
|
||||
def start_core(self, mixer, backends):
|
||||
logger.info('Starting Mopidy core')
|
||||
return Core.start(audio=audio, backends=backends).proxy()
|
||||
return Core.start(mixer=mixer, backends=backends).proxy()
|
||||
|
||||
def start_frontends(self, config, frontend_classes, core):
|
||||
logger.info(
|
||||
@ -306,7 +355,13 @@ class RootCommand(Command):
|
||||
', '.join(f.__name__ for f in frontend_classes) or 'none')
|
||||
|
||||
for frontend_class in frontend_classes:
|
||||
frontend_class.start(config=config, core=core)
|
||||
try:
|
||||
frontend_class.start(config=config, core=core)
|
||||
except exceptions.FrontendError as exc:
|
||||
logger.error(
|
||||
'Frontend (%s) initialization error: %s',
|
||||
frontend_class.__name__, exc.message)
|
||||
raise
|
||||
|
||||
def stop_frontends(self, frontend_classes):
|
||||
logger.info('Stopping Mopidy frontends')
|
||||
@ -326,6 +381,10 @@ class RootCommand(Command):
|
||||
logger.info('Stopping Mopidy audio')
|
||||
process.stop_actors_by_class(Audio)
|
||||
|
||||
def stop_mixer(self, mixer_class):
|
||||
logger.info('Stopping Mopidy mixer')
|
||||
process.stop_actors_by_class(mixer_class)
|
||||
|
||||
|
||||
class ConfigCommand(Command):
|
||||
help = 'Show currently active configuration.'
|
||||
|
||||
@ -15,6 +15,7 @@ from mopidy.utils import path, versioning
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_logging_schema = ConfigSchema('logging')
|
||||
_logging_schema['color'] = Boolean()
|
||||
_logging_schema['console_format'] = String()
|
||||
_logging_schema['debug_format'] = String()
|
||||
_logging_schema['debug_file'] = Path()
|
||||
@ -24,7 +25,7 @@ _loglevels_schema = LogLevelConfigSchema('loglevels')
|
||||
|
||||
_audio_schema = ConfigSchema('audio')
|
||||
_audio_schema['mixer'] = String()
|
||||
_audio_schema['mixer_track'] = String(optional=True)
|
||||
_audio_schema['mixer_track'] = Deprecated()
|
||||
_audio_schema['mixer_volume'] = Integer(optional=True, minimum=0, maximum=100)
|
||||
_audio_schema['output'] = String()
|
||||
_audio_schema['visualizer'] = String(optional=True)
|
||||
@ -38,7 +39,7 @@ _proxy_schema['username'] = String(optional=True)
|
||||
_proxy_schema['password'] = Secret(optional=True)
|
||||
|
||||
# NOTE: if multiple outputs ever comes something like LogLevelConfigSchema
|
||||
#_outputs_schema = config.AudioOutputConfigSchema()
|
||||
# _outputs_schema = config.AudioOutputConfigSchema()
|
||||
|
||||
_schemas = [_logging_schema, _loglevels_schema, _audio_schema, _proxy_schema]
|
||||
|
||||
|
||||
@ -1,126 +0,0 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from mopidy import config as config_lib, ext
|
||||
from mopidy.utils import path
|
||||
|
||||
|
||||
def load():
|
||||
settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
|
||||
print('Checking %s' % settings_file)
|
||||
|
||||
setting_globals = {}
|
||||
try:
|
||||
execfile(settings_file, setting_globals)
|
||||
except Exception as e:
|
||||
print('Problem loading settings: %s' % e)
|
||||
return setting_globals
|
||||
|
||||
|
||||
def convert(settings):
|
||||
config = {}
|
||||
|
||||
def helper(confval, setting_name):
|
||||
if settings.get(setting_name) is not None:
|
||||
section, key = confval.split('/')
|
||||
config.setdefault(section, {})[key] = settings[setting_name]
|
||||
|
||||
# Perform all the simple mappings using our helper:
|
||||
|
||||
helper('logging/console_format', 'CONSOLE_LOG_FORMAT')
|
||||
helper('logging/debug_format', 'DEBUG_LOG_FORMAT')
|
||||
helper('logging/debug_file', 'DEBUG_LOG_FILENAME')
|
||||
|
||||
helper('audio/mixer', 'MIXER')
|
||||
helper('audio/mixer_track', 'MIXER_TRACK')
|
||||
helper('audio/mixer_volume', 'MIXER_VOLUME')
|
||||
helper('audio/output', 'OUTPUT')
|
||||
|
||||
helper('proxy/hostname', 'SPOTIFY_PROXY_HOST')
|
||||
helper('proxy/port', 'SPOTIFY_PROXY_PORT')
|
||||
helper('proxy/username', 'SPOTIFY_PROXY_USERNAME')
|
||||
helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD')
|
||||
|
||||
helper('local/media_dir', 'LOCAL_MUSIC_PATH')
|
||||
helper('local/playlists_dir', 'LOCAL_PLAYLIST_PATH')
|
||||
|
||||
helper('spotify/username', 'SPOTIFY_USERNAME')
|
||||
helper('spotify/password', 'SPOTIFY_PASSWORD')
|
||||
helper('spotify/bitrate', 'SPOTIFY_BITRATE')
|
||||
helper('spotify/timeout', 'SPOTIFY_TIMEOUT')
|
||||
helper('spotify/cache_dir', 'SPOTIFY_CACHE_PATH')
|
||||
|
||||
helper('stream/protocols', 'STREAM_PROTOCOLS')
|
||||
|
||||
helper('http/hostname', 'HTTP_SERVER_HOSTNAME')
|
||||
helper('http/port', 'HTTP_SERVER_PORT')
|
||||
helper('http/static_dir', 'HTTP_SERVER_STATIC_DIR')
|
||||
|
||||
helper('mpd/hostname', 'MPD_SERVER_HOSTNAME')
|
||||
helper('mpd/port', 'MPD_SERVER_PORT')
|
||||
helper('mpd/password', 'MPD_SERVER_PASSWORD')
|
||||
helper('mpd/max_connections', 'MPD_SERVER_MAX_CONNECTIONS')
|
||||
helper('mpd/connection_timeout', 'MPD_SERVER_CONNECTION_TIMEOUT')
|
||||
|
||||
helper('mpris/desktop_file', 'DESKTOP_FILE')
|
||||
|
||||
helper('scrobbler/username', 'LASTFM_USERNAME')
|
||||
helper('scrobbler/password', 'LASTFM_PASSWORD')
|
||||
|
||||
# Assume FRONTENDS/BACKENDS = None implies all enabled, otherwise disable
|
||||
# if our module path is missing from the setting.
|
||||
|
||||
frontends = settings.get('FRONTENDS')
|
||||
if frontends is not None:
|
||||
if 'mopidy.frontends.http.HttpFrontend' not in frontends:
|
||||
config.setdefault('http', {})['enabled'] = False
|
||||
if 'mopidy.frontends.mpd.MpdFrontend' not in frontends:
|
||||
config.setdefault('mpd', {})['enabled'] = False
|
||||
if 'mopidy.frontends.lastfm.LastfmFrontend' not in frontends:
|
||||
config.setdefault('scrobbler', {})['enabled'] = False
|
||||
if 'mopidy.frontends.mpris.MprisFrontend' not in frontends:
|
||||
config.setdefault('mpris', {})['enabled'] = False
|
||||
|
||||
backends = settings.get('BACKENDS')
|
||||
if backends is not None:
|
||||
if 'mopidy.backends.local.LocalBackend' not in backends:
|
||||
config.setdefault('local', {})['enabled'] = False
|
||||
if 'mopidy.backends.spotify.SpotifyBackend' not in backends:
|
||||
config.setdefault('spotify', {})['enabled'] = False
|
||||
if 'mopidy.backends.stream.StreamBackend' not in backends:
|
||||
config.setdefault('stream', {})['enabled'] = False
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def main():
|
||||
settings = load()
|
||||
if not settings:
|
||||
return
|
||||
|
||||
config = convert(settings)
|
||||
|
||||
known = [
|
||||
'spotify', 'scrobbler', 'mpd', 'mpris', 'local', 'stream', 'http']
|
||||
extensions = [e for e in ext.load_extensions() if e.ext_name in known]
|
||||
|
||||
print(b'Converted config:\n')
|
||||
print(config_lib.format(config, extensions))
|
||||
|
||||
conf_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
|
||||
if os.path.exists(conf_file):
|
||||
print('%s exists, exiting.' % conf_file)
|
||||
sys.exit(1)
|
||||
|
||||
print('Write new config to %s? [yN]' % conf_file, end=' ')
|
||||
if raw_input() != 'y':
|
||||
print('Not saving, exiting.')
|
||||
sys.exit(0)
|
||||
|
||||
serialized_config = config_lib.format(config, extensions, display=False)
|
||||
with io.open(conf_file, 'wb') as filehandle:
|
||||
filehandle.write(serialized_config)
|
||||
print('Done.')
|
||||
@ -1,4 +1,5 @@
|
||||
[logging]
|
||||
color = true
|
||||
console_format = %(levelname)-8s %(message)s
|
||||
debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s
|
||||
debug_file = mopidy.log
|
||||
@ -6,7 +7,6 @@ config_file =
|
||||
|
||||
[audio]
|
||||
mixer = software
|
||||
mixer_track =
|
||||
mixer_volume =
|
||||
output = autoaudiosink
|
||||
visualizer =
|
||||
|
||||
@ -4,8 +4,8 @@ import logging
|
||||
import re
|
||||
import socket
|
||||
|
||||
from mopidy.utils import path
|
||||
from mopidy.config import validators
|
||||
from mopidy.utils import path
|
||||
|
||||
|
||||
def decode(value):
|
||||
@ -151,7 +151,13 @@ class Boolean(ConfigValue):
|
||||
true_values = ('1', 'yes', 'true', 'on')
|
||||
false_values = ('0', 'no', 'false', 'off')
|
||||
|
||||
def __init__(self, optional=False):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value):
|
||||
validators.validate_required(value, self._required)
|
||||
if not value:
|
||||
return None
|
||||
if value.lower() in self.true_values:
|
||||
return True
|
||||
elif value.lower() in self.false_values:
|
||||
@ -185,6 +191,8 @@ class List(ConfigValue):
|
||||
return tuple(values)
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
if not value:
|
||||
return b''
|
||||
return b'\n ' + b'\n '.join(encode(v) for v in value if v)
|
||||
|
||||
|
||||
|
||||
@ -5,18 +5,20 @@ import itertools
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import audio, backend
|
||||
from mopidy import audio, backend, mixer
|
||||
from mopidy.audio import PlaybackState
|
||||
from mopidy.core.library import LibraryController
|
||||
from mopidy.core.listener import CoreListener
|
||||
from mopidy.core.playback import PlaybackController
|
||||
from mopidy.core.playlists import PlaylistsController
|
||||
from mopidy.core.tracklist import TracklistController
|
||||
from mopidy.utils import versioning
|
||||
|
||||
from .library import LibraryController
|
||||
from .listener import CoreListener
|
||||
from .playback import PlaybackController
|
||||
from .playlists import PlaylistsController
|
||||
from .tracklist import TracklistController
|
||||
|
||||
class Core(
|
||||
pykka.ThreadingActor, audio.AudioListener, backend.BackendListener,
|
||||
mixer.MixerListener):
|
||||
|
||||
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
library = None
|
||||
"""The library controller. An instance of
|
||||
:class:`mopidy.core.LibraryController`."""
|
||||
@ -33,7 +35,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
"""The tracklist controller. An instance of
|
||||
:class:`mopidy.core.TracklistController`."""
|
||||
|
||||
def __init__(self, audio=None, backends=None):
|
||||
def __init__(self, mixer=None, backends=None):
|
||||
super(Core, self).__init__()
|
||||
|
||||
self.backends = Backends(backends)
|
||||
@ -41,7 +43,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
self.library = LibraryController(backends=self.backends, core=self)
|
||||
|
||||
self.playback = PlaybackController(
|
||||
audio=audio, backends=self.backends, core=self)
|
||||
mixer=mixer, backends=self.backends, core=self)
|
||||
|
||||
self.playlists = PlaylistsController(
|
||||
backends=self.backends, core=self)
|
||||
@ -66,14 +68,17 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
def reached_end_of_stream(self):
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
def state_changed(self, old_state, new_state):
|
||||
def state_changed(self, old_state, new_state, target_state):
|
||||
# XXX: This is a temporary fix for issue #232 while we wait for a more
|
||||
# permanent solution with the implementation of issue #234. When the
|
||||
# Spotify play token is lost, the Spotify backend pauses audio
|
||||
# playback, but mopidy.core doesn't know this, so we need to update
|
||||
# mopidy.core's state to match the actual state in mopidy.audio. If we
|
||||
# don't do this, clients will think that we're still playing.
|
||||
if (new_state == PlaybackState.PAUSED
|
||||
|
||||
# We ignore cases when target state is set as this is buffering
|
||||
# updates (at least for now) and we need to get #234 fixed...
|
||||
if (new_state == PlaybackState.PAUSED and not target_state
|
||||
and self.playback.state != PlaybackState.PAUSED):
|
||||
self.playback.state = new_state
|
||||
self.playback._trigger_track_playback_paused()
|
||||
@ -82,6 +87,14 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
# Forward event from backend to frontends
|
||||
CoreListener.send('playlists_loaded')
|
||||
|
||||
def volume_changed(self, volume):
|
||||
# Forward event from mixer to frontends
|
||||
CoreListener.send('volume_changed', volume=volume)
|
||||
|
||||
def mute_changed(self, mute):
|
||||
# Forward event from mixer to frontends
|
||||
CoreListener.send('mute_changed', mute=mute)
|
||||
|
||||
|
||||
class Backends(list):
|
||||
def __init__(self, backends):
|
||||
@ -93,26 +106,26 @@ class Backends(list):
|
||||
self.with_playlists = collections.OrderedDict()
|
||||
|
||||
backends_by_scheme = {}
|
||||
name = lambda backend: backend.actor_ref.actor_class.__name__
|
||||
name = lambda b: b.actor_ref.actor_class.__name__
|
||||
|
||||
for backend in backends:
|
||||
has_library = backend.has_library().get()
|
||||
has_library_browse = backend.has_library_browse().get()
|
||||
has_playback = backend.has_playback().get()
|
||||
has_playlists = backend.has_playlists().get()
|
||||
for b in backends:
|
||||
has_library = b.has_library().get()
|
||||
has_library_browse = b.has_library_browse().get()
|
||||
has_playback = b.has_playback().get()
|
||||
has_playlists = b.has_playlists().get()
|
||||
|
||||
for scheme in backend.uri_schemes.get():
|
||||
for scheme in b.uri_schemes.get():
|
||||
assert scheme not in backends_by_scheme, (
|
||||
'Cannot add URI scheme %s for %s, '
|
||||
'it is already handled by %s'
|
||||
) % (scheme, name(backend), name(backends_by_scheme[scheme]))
|
||||
backends_by_scheme[scheme] = backend
|
||||
) % (scheme, name(b), name(backends_by_scheme[scheme]))
|
||||
backends_by_scheme[scheme] = b
|
||||
|
||||
if has_library:
|
||||
self.with_library[scheme] = backend
|
||||
self.with_library[scheme] = b
|
||||
if has_library_browse:
|
||||
self.with_library_browse[scheme] = backend
|
||||
self.with_library_browse[scheme] = b
|
||||
if has_playback:
|
||||
self.with_playback[scheme] = backend
|
||||
self.with_playback[scheme] = b
|
||||
if has_playlists:
|
||||
self.with_playlists[scheme] = backend
|
||||
self.with_playlists[scheme] = b
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import operator
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
@ -62,7 +63,8 @@ class LibraryController(object):
|
||||
"""
|
||||
if uri is None:
|
||||
backends = self.backends.with_library_browse.values()
|
||||
return [b.library.root_directory.get() for b in backends]
|
||||
unique_dirs = {b.library.root_directory.get() for b in backends}
|
||||
return sorted(unique_dirs, key=operator.attrgetter('name'))
|
||||
|
||||
scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_library_browse.get(scheme)
|
||||
|
||||
@ -4,8 +4,7 @@ import logging
|
||||
import urlparse
|
||||
|
||||
from mopidy.audio import PlaybackState
|
||||
|
||||
from . import listener
|
||||
from mopidy.core import listener
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -14,8 +13,8 @@ logger = logging.getLogger(__name__)
|
||||
class PlaybackController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, audio, backends, core):
|
||||
self.audio = audio
|
||||
def __init__(self, mixer, backends, core):
|
||||
self.mixer = mixer
|
||||
self.backends = backends
|
||||
self.core = core
|
||||
|
||||
@ -30,7 +29,7 @@ class PlaybackController(object):
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.with_playback.get(uri_scheme, None)
|
||||
|
||||
### Properties
|
||||
# Properties
|
||||
|
||||
def get_current_tl_track(self):
|
||||
return self.current_tl_track
|
||||
@ -89,45 +88,43 @@ class PlaybackController(object):
|
||||
"""Time position in milliseconds."""
|
||||
|
||||
def get_volume(self):
|
||||
if self.audio:
|
||||
return self.audio.get_volume().get()
|
||||
if self.mixer:
|
||||
return self.mixer.get_volume().get()
|
||||
else:
|
||||
# For testing
|
||||
return self._volume
|
||||
|
||||
def set_volume(self, volume):
|
||||
if self.audio:
|
||||
self.audio.set_volume(volume)
|
||||
if self.mixer:
|
||||
self.mixer.set_volume(volume)
|
||||
else:
|
||||
# For testing
|
||||
self._volume = volume
|
||||
|
||||
self._trigger_volume_changed(volume)
|
||||
|
||||
volume = property(get_volume, set_volume)
|
||||
"""Volume as int in range [0..100] or :class:`None`"""
|
||||
"""Volume as int in range [0..100] or :class:`None` if unknown. The volume
|
||||
scale is linear.
|
||||
"""
|
||||
|
||||
def get_mute(self):
|
||||
if self.audio:
|
||||
return self.audio.get_mute().get()
|
||||
if self.mixer:
|
||||
return self.mixer.get_mute().get()
|
||||
else:
|
||||
# For testing
|
||||
return self._mute
|
||||
|
||||
def set_mute(self, value):
|
||||
value = bool(value)
|
||||
if self.audio:
|
||||
self.audio.set_mute(value)
|
||||
if self.mixer:
|
||||
self.mixer.set_mute(value)
|
||||
else:
|
||||
# For testing
|
||||
self._mute = value
|
||||
|
||||
self._trigger_mute_changed(value)
|
||||
|
||||
mute = property(get_mute, set_mute)
|
||||
"""Mute state as a :class:`True` if muted, :class:`False` otherwise"""
|
||||
|
||||
### Methods
|
||||
# Methods
|
||||
|
||||
def change_track(self, tl_track, on_error_step=1):
|
||||
"""
|
||||
@ -352,14 +349,6 @@ class PlaybackController(object):
|
||||
'playback_state_changed',
|
||||
old_state=old_state, new_state=new_state)
|
||||
|
||||
def _trigger_volume_changed(self, volume):
|
||||
logger.debug('Triggering volume changed event')
|
||||
listener.CoreListener.send('volume_changed', volume=volume)
|
||||
|
||||
def _trigger_mute_changed(self, mute):
|
||||
logger.debug('Triggering mute changed event')
|
||||
listener.CoreListener.send('mute_changed', mute=mute)
|
||||
|
||||
def _trigger_seeked(self, time_position):
|
||||
logger.debug('Triggering seeked event')
|
||||
listener.CoreListener.send('seeked', time_position=time_position)
|
||||
|
||||
@ -4,10 +4,9 @@ import collections
|
||||
import logging
|
||||
import random
|
||||
|
||||
from mopidy.core import listener
|
||||
from mopidy.models import TlTrack
|
||||
|
||||
from . import listener
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -23,7 +22,7 @@ class TracklistController(object):
|
||||
|
||||
self._shuffled = []
|
||||
|
||||
### Properties
|
||||
# Properties
|
||||
|
||||
def get_tl_tracks(self):
|
||||
return self._tl_tracks[:]
|
||||
@ -136,7 +135,7 @@ class TracklistController(object):
|
||||
Playback continues after current song.
|
||||
"""
|
||||
|
||||
### Methods
|
||||
# Methods
|
||||
|
||||
def index(self, tl_track):
|
||||
"""
|
||||
|
||||
@ -16,9 +16,21 @@ class MopidyException(Exception):
|
||||
self._message = message
|
||||
|
||||
|
||||
class BackendError(MopidyException):
|
||||
pass
|
||||
|
||||
|
||||
class ExtensionError(MopidyException):
|
||||
pass
|
||||
|
||||
|
||||
class FrontendError(MopidyException):
|
||||
pass
|
||||
|
||||
|
||||
class MixerError(MopidyException):
|
||||
pass
|
||||
|
||||
|
||||
class ScannerError(MopidyException):
|
||||
pass
|
||||
|
||||
@ -2,10 +2,10 @@ from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import logging
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy import config as config_lib
|
||||
from mopidy import config as config_lib, exceptions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -99,42 +99,6 @@ class Extension(object):
|
||||
:param registry: the extension registry
|
||||
:type registry: :class:`Registry`
|
||||
"""
|
||||
for backend_class in self.get_backend_classes():
|
||||
registry.add('backend', backend_class)
|
||||
|
||||
for frontend_class in self.get_frontend_classes():
|
||||
registry.add('frontend', frontend_class)
|
||||
|
||||
self.register_gstreamer_elements()
|
||||
|
||||
def get_frontend_classes(self):
|
||||
"""List of frontend actor classes
|
||||
|
||||
.. deprecated:: 0.18
|
||||
Use :meth:`setup` instead.
|
||||
|
||||
:returns: list of :class:`pykka.Actor` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_backend_classes(self):
|
||||
"""List of backend actor classes
|
||||
|
||||
.. deprecated:: 0.18
|
||||
Use :meth:`setup` instead.
|
||||
|
||||
:returns: list of :class:`~mopidy.backend.Backend` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
"""Hook for registering custom GStreamer elements.
|
||||
|
||||
.. deprecated:: 0.18
|
||||
Use :meth:`setup` instead.
|
||||
|
||||
:returns: :class:`None`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@ -1,40 +1,48 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, exceptions, ext
|
||||
from mopidy import config as config_lib, exceptions, ext
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-HTTP'
|
||||
ext_name = 'http'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
return config_lib.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['hostname'] = config.Hostname()
|
||||
schema['port'] = config.Port()
|
||||
schema['static_dir'] = config.Path(optional=True)
|
||||
schema['zeroconf'] = config.String(optional=True)
|
||||
schema['hostname'] = config_lib.Hostname()
|
||||
schema['port'] = config_lib.Port()
|
||||
schema['static_dir'] = config_lib.Path(optional=True)
|
||||
schema['zeroconf'] = config_lib.String(optional=True)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
try:
|
||||
import cherrypy # noqa
|
||||
import tornado.web # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('cherrypy library not found', e)
|
||||
|
||||
try:
|
||||
import ws4py # noqa
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('ws4py library not found', e)
|
||||
raise exceptions.ExtensionError('tornado library not found', e)
|
||||
|
||||
def setup(self, registry):
|
||||
from .actor import HttpFrontend
|
||||
from .handlers import make_mopidy_app_factory
|
||||
|
||||
HttpFrontend.apps = registry['http:app']
|
||||
HttpFrontend.statics = registry['http:static']
|
||||
|
||||
registry.add('frontend', HttpFrontend)
|
||||
registry.add('http:app', {
|
||||
'name': 'mopidy',
|
||||
'factory': make_mopidy_app_factory(
|
||||
registry['http:app'], registry['http:static']),
|
||||
})
|
||||
|
||||
@ -1,129 +1,182 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
import cherrypy
|
||||
import pykka
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
|
||||
from mopidy import models, zeroconf
|
||||
import tornado.httpserver
|
||||
import tornado.ioloop
|
||||
import tornado.netutil
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
|
||||
from mopidy import exceptions, models, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from . import ws
|
||||
from mopidy.http import handlers
|
||||
from mopidy.utils import encoding, formatting, network
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
apps = []
|
||||
statics = []
|
||||
|
||||
def __init__(self, config, core):
|
||||
super(HttpFrontend, self).__init__()
|
||||
self.config = config
|
||||
self.core = core
|
||||
|
||||
self.hostname = config['http']['hostname']
|
||||
self.hostname = network.format_hostname(config['http']['hostname'])
|
||||
self.port = config['http']['port']
|
||||
tornado_hostname = config['http']['hostname']
|
||||
if tornado_hostname == '::':
|
||||
tornado_hostname = None
|
||||
|
||||
try:
|
||||
logger.debug('Starting HTTP server')
|
||||
sockets = tornado.netutil.bind_sockets(self.port, tornado_hostname)
|
||||
self.server = HttpServer(
|
||||
config=config, core=core, sockets=sockets,
|
||||
apps=self.apps, statics=self.statics)
|
||||
except IOError as error:
|
||||
raise exceptions.FrontendError(
|
||||
'HTTP server startup failed: %s' %
|
||||
encoding.locale_decode(error))
|
||||
|
||||
self.zeroconf_name = config['http']['zeroconf']
|
||||
self.zeroconf_service = None
|
||||
|
||||
self._setup_server()
|
||||
self._setup_websocket_plugin()
|
||||
app = self._create_app()
|
||||
self._setup_logging(app)
|
||||
|
||||
def _setup_server(self):
|
||||
cherrypy.config.update({
|
||||
'engine.autoreload_on': False,
|
||||
'server.socket_host': self.hostname,
|
||||
'server.socket_port': self.port,
|
||||
})
|
||||
|
||||
def _setup_websocket_plugin(self):
|
||||
WebSocketPlugin(cherrypy.engine).subscribe()
|
||||
cherrypy.tools.websocket = WebSocketTool()
|
||||
|
||||
def _create_app(self):
|
||||
root = RootResource()
|
||||
root.mopidy = MopidyResource()
|
||||
root.mopidy.ws = ws.WebSocketResource(self.core)
|
||||
|
||||
if self.config['http']['static_dir']:
|
||||
static_dir = self.config['http']['static_dir']
|
||||
else:
|
||||
static_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
logger.debug('HTTP server will serve "%s" at /', static_dir)
|
||||
|
||||
mopidy_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
favicon = os.path.join(mopidy_dir, 'favicon.png')
|
||||
|
||||
config = {
|
||||
b'/': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.index': 'index.html',
|
||||
'tools.staticdir.dir': static_dir,
|
||||
},
|
||||
b'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': favicon,
|
||||
},
|
||||
b'/mopidy': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.index': 'mopidy.html',
|
||||
'tools.staticdir.dir': mopidy_dir,
|
||||
},
|
||||
b'/mopidy/ws': {
|
||||
'tools.websocket.on': True,
|
||||
'tools.websocket.handler_cls': ws.WebSocketHandler,
|
||||
},
|
||||
}
|
||||
|
||||
return cherrypy.tree.mount(root, '/', config)
|
||||
|
||||
def _setup_logging(self, app):
|
||||
cherrypy.log.access_log.setLevel(logging.NOTSET)
|
||||
cherrypy.log.error_log.setLevel(logging.NOTSET)
|
||||
cherrypy.log.screen = False
|
||||
|
||||
app.log.access_log.setLevel(logging.NOTSET)
|
||||
app.log.error_log.setLevel(logging.NOTSET)
|
||||
self.zeroconf_http = None
|
||||
self.zeroconf_mopidy_http = None
|
||||
|
||||
def on_start(self):
|
||||
logger.debug('Starting HTTP server')
|
||||
cherrypy.engine.start()
|
||||
logger.info('HTTP server running at %s', cherrypy.server.base())
|
||||
logger.info(
|
||||
'HTTP server running at [%s]:%s', self.hostname, self.port)
|
||||
self.server.start()
|
||||
|
||||
if self.zeroconf_name:
|
||||
self.zeroconf_service = zeroconf.Zeroconf(
|
||||
self.zeroconf_http = zeroconf.Zeroconf(
|
||||
stype='_http._tcp', name=self.zeroconf_name,
|
||||
host=self.hostname, port=self.port)
|
||||
|
||||
if self.zeroconf_service.publish():
|
||||
logger.debug(
|
||||
'Registered HTTP with Zeroconf as "%s"',
|
||||
self.zeroconf_service.name)
|
||||
else:
|
||||
logger.debug('Registering HTTP with Zeroconf failed.')
|
||||
self.zeroconf_mopidy_http = zeroconf.Zeroconf(
|
||||
stype='_mopidy-http._tcp', name=self.zeroconf_name,
|
||||
host=self.hostname, port=self.port)
|
||||
self.zeroconf_http.publish()
|
||||
self.zeroconf_mopidy_http.publish()
|
||||
|
||||
def on_stop(self):
|
||||
if self.zeroconf_service:
|
||||
self.zeroconf_service.unpublish()
|
||||
if self.zeroconf_http:
|
||||
self.zeroconf_http.unpublish()
|
||||
if self.zeroconf_mopidy_http:
|
||||
self.zeroconf_mopidy_http.unpublish()
|
||||
|
||||
logger.debug('Stopping HTTP server')
|
||||
cherrypy.engine.exit()
|
||||
logger.info('Stopped HTTP server')
|
||||
self.server.stop()
|
||||
|
||||
def on_event(self, name, **data):
|
||||
event = data
|
||||
event['event'] = name
|
||||
message = json.dumps(event, cls=models.ModelJSONEncoder)
|
||||
cherrypy.engine.publish('websocket-broadcast', TextMessage(message))
|
||||
on_event(name, **data)
|
||||
|
||||
|
||||
class RootResource(object):
|
||||
pass
|
||||
def on_event(name, **data):
|
||||
event = data
|
||||
event['event'] = name
|
||||
message = json.dumps(event, cls=models.ModelJSONEncoder)
|
||||
handlers.WebSocketHandler.broadcast(message)
|
||||
|
||||
|
||||
class MopidyResource(object):
|
||||
pass
|
||||
class HttpServer(threading.Thread):
|
||||
name = 'HttpServer'
|
||||
|
||||
def __init__(self, config, core, sockets, apps, statics):
|
||||
super(HttpServer, self).__init__()
|
||||
|
||||
self.config = config
|
||||
self.core = core
|
||||
self.sockets = sockets
|
||||
self.apps = apps
|
||||
self.statics = statics
|
||||
|
||||
self.app = None
|
||||
self.server = None
|
||||
|
||||
def run(self):
|
||||
self.app = tornado.web.Application(self._get_request_handlers())
|
||||
self.server = tornado.httpserver.HTTPServer(self.app)
|
||||
self.server.add_sockets(self.sockets)
|
||||
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
logger.debug('Stopped HTTP server')
|
||||
|
||||
def stop(self):
|
||||
logger.debug('Stopping HTTP server')
|
||||
tornado.ioloop.IOLoop.instance().add_callback(
|
||||
tornado.ioloop.IOLoop.instance().stop)
|
||||
|
||||
def _get_request_handlers(self):
|
||||
request_handlers = []
|
||||
request_handlers.extend(self._get_app_request_handlers())
|
||||
request_handlers.extend(self._get_static_request_handlers())
|
||||
request_handlers.extend(self._get_mopidy_request_handlers())
|
||||
|
||||
logger.debug(
|
||||
'HTTP routes from extensions: %s',
|
||||
formatting.indent('\n'.join(
|
||||
'%r: %r' % (r[0], r[1]) for r in request_handlers)))
|
||||
|
||||
return request_handlers
|
||||
|
||||
def _get_app_request_handlers(self):
|
||||
result = []
|
||||
for app in self.apps:
|
||||
result.append((
|
||||
r'/%s' % app['name'],
|
||||
handlers.AddSlashHandler
|
||||
))
|
||||
request_handlers = app['factory'](self.config, self.core)
|
||||
for handler in request_handlers:
|
||||
handler = list(handler)
|
||||
handler[0] = '/%s%s' % (app['name'], handler[0])
|
||||
result.append(tuple(handler))
|
||||
logger.debug('Loaded HTTP extension: %s', app['name'])
|
||||
return result
|
||||
|
||||
def _get_static_request_handlers(self):
|
||||
result = []
|
||||
for static in self.statics:
|
||||
result.append((
|
||||
r'/%s' % static['name'],
|
||||
handlers.AddSlashHandler
|
||||
))
|
||||
result.append((
|
||||
r'/%s/(.*)' % static['name'],
|
||||
handlers.StaticFileHandler,
|
||||
{
|
||||
'path': static['path'],
|
||||
'default_filename': 'index.html'
|
||||
}
|
||||
))
|
||||
logger.debug('Loaded static HTTP extension: %s', static['name'])
|
||||
return result
|
||||
|
||||
def _get_mopidy_request_handlers(self):
|
||||
# Either default Mopidy or user defined path to files
|
||||
|
||||
static_dir = self.config['http']['static_dir']
|
||||
|
||||
if static_dir and not os.path.exists(static_dir):
|
||||
logger.warning(
|
||||
'Configured http/static_dir %s does not exist. '
|
||||
'Falling back to default HTTP handler.', static_dir)
|
||||
static_dir = None
|
||||
|
||||
if static_dir:
|
||||
return [(r'/(.*)', handlers.StaticFileHandler, {
|
||||
'path': self.config['http']['static_dir'],
|
||||
'default_filename': 'index.html',
|
||||
})]
|
||||
else:
|
||||
return [(r'/', tornado.web.RedirectHandler, {
|
||||
'url': '/mopidy/',
|
||||
'permanent': False,
|
||||
})]
|
||||
|
||||
31
mopidy/http/data/clients.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mopidy</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy</h1>
|
||||
|
||||
<p>This web server is a part of the Mopidy music server. To learn more
|
||||
about Mopidy, please visit
|
||||
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Web clients</h2>
|
||||
|
||||
<ul>
|
||||
{% for app in apps %}
|
||||
<li><a href="/{{ url_escape(app) }}/">{{ escape(app) }}</a></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
|
||||
<p>Web clients which are installed as Mopidy extensions will
|
||||
automatically appear here.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@ -1,29 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mopidy HTTP frontend</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy HTTP frontend</h1>
|
||||
|
||||
<p>This web server is a part of the music server Mopidy. To learn more
|
||||
about Mopidy, please visit
|
||||
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Static content serving</h2>
|
||||
|
||||
<p>To see your own content instead of this placeholder page, change the
|
||||
setting <tt>HTTP_SERVER_STATIC_DIR</tt> to point to the directory
|
||||
containing your static files. This can be used to host e.g. a pure
|
||||
HTML/CSS/JavaScript Mopidy client.</p>
|
||||
|
||||
<p>If you replace this page with your own content, the Mopidy resources
|
||||
at <a href="/mopidy/">/mopidy/</a> will still be available.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,40 +1,17 @@
|
||||
html {
|
||||
background: #e8ecef;
|
||||
background: #f8f8f8;
|
||||
color: #555;
|
||||
font-family: "Droid Serif", "Georgia", "Times New Roman", "Palatino",
|
||||
"Hoefler Text", "Baskerville", serif;
|
||||
font-size: 150%;
|
||||
font-family: Geneva, Tahoma, Verdana, sans-serif;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
body {
|
||||
max-width: 20em;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
div.box {
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 5px 5px 5px #d8dcdf;
|
||||
margin: 2em 0;
|
||||
padding: 1em;
|
||||
}
|
||||
div.box.focus {
|
||||
background: #465158;
|
||||
color: #e8ecef;
|
||||
}
|
||||
div.icon {
|
||||
float: right;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: "Ubuntu", "Arial", "Helvetica", "Lucida Grande",
|
||||
"Verdana", "Gill Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
h2 {
|
||||
margin: 0.2em 0 0;
|
||||
}
|
||||
p.next {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
@ -43,20 +20,18 @@ a {
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
code, pre {
|
||||
font-family: "Droid Sans Mono", Menlo, Courier New, Courier, Mono, monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.2em;
|
||||
padding: 0.5em 1em;
|
||||
margin: 1em 0;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
|
||||
.box {
|
||||
background: white;
|
||||
box-shadow: 0px 5px 5px #f0f0f0;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
.box code,
|
||||
.box pre {
|
||||
background: #e8ecef;
|
||||
color: #555;
|
||||
.box.focus {
|
||||
background: #465158;
|
||||
color: #e8ecef;
|
||||
}
|
||||
|
||||
.box a {
|
||||
color: #465158;
|
||||
}
|
||||
@ -66,10 +41,3 @@ code, pre {
|
||||
.box.focus a {
|
||||
color: #e8ecef;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
#ws-console {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mopidy HTTP frontend</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy HTTP frontend</h1>
|
||||
|
||||
<p>This web server is a part of the music server Mopidy. To learn more
|
||||
about Mopidy, please visit <a
|
||||
href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>WebSocket endpoint</h2>
|
||||
|
||||
<p>Mopidy has a WebSocket endpoint at <tt>/mopidy/ws/</tt>. You can use
|
||||
this end point to access Mopidy's full API, and to get notified about
|
||||
events happening in Mopidy.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Example</h2>
|
||||
|
||||
<p>Here you can see events arriving from Mopidy in real time:</p>
|
||||
|
||||
<pre id="ws-console"></pre>
|
||||
|
||||
<p>Nothing to see? Try playing a track using your MPD client.</p>
|
||||
</div>
|
||||
|
||||
<div class="box focus">
|
||||
<h2>Documentation</h2>
|
||||
|
||||
<p>For more information, please refer to the Mopidy documentation at
|
||||
<a href="http://docs.mopidy.com/">docs.mopidy.com</a>.</p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var ws = new WebSocket("ws://" + document.location.host + "/mopidy/ws/");
|
||||
ws.onmessage = function (message) {
|
||||
var console = document.getElementById('ws-console');
|
||||
var newLine = (new Date()).toLocaleTimeString() + ": " +
|
||||
message.data + "\n";
|
||||
console.innerHTML = newLine + console.innerHTML;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
4
mopidy/http/data/mopidy.min.js
vendored
180
mopidy/http/handlers.py
Normal file
@ -0,0 +1,180 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
|
||||
import mopidy
|
||||
from mopidy import core, models
|
||||
from mopidy.utils import jsonrpc
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_mopidy_app_factory(apps, statics):
|
||||
def mopidy_app_factory(config, core):
|
||||
return [
|
||||
(r'/ws/?', WebSocketHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/rpc', JsonRpcHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/(.+)', StaticFileHandler, {
|
||||
'path': os.path.join(os.path.dirname(__file__), 'data'),
|
||||
}),
|
||||
(r'/', ClientListHandler, {
|
||||
'apps': apps,
|
||||
'statics': statics,
|
||||
}),
|
||||
]
|
||||
return mopidy_app_factory
|
||||
|
||||
|
||||
def make_jsonrpc_wrapper(core_actor):
|
||||
inspector = jsonrpc.JsonRpcInspector(
|
||||
objects={
|
||||
'core.get_uri_schemes': core.Core.get_uri_schemes,
|
||||
'core.get_version': core.Core.get_version,
|
||||
'core.library': core.LibraryController,
|
||||
'core.playback': core.PlaybackController,
|
||||
'core.playlists': core.PlaylistsController,
|
||||
'core.tracklist': core.TracklistController,
|
||||
})
|
||||
return jsonrpc.JsonRpcWrapper(
|
||||
objects={
|
||||
'core.describe': inspector.describe,
|
||||
'core.get_uri_schemes': core_actor.get_uri_schemes,
|
||||
'core.get_version': core_actor.get_version,
|
||||
'core.library': core_actor.library,
|
||||
'core.playback': core_actor.playback,
|
||||
'core.playlists': core_actor.playlists,
|
||||
'core.tracklist': core_actor.tracklist,
|
||||
},
|
||||
decoders=[models.model_json_decoder],
|
||||
encoders=[models.ModelJSONEncoder]
|
||||
)
|
||||
|
||||
|
||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
|
||||
# XXX This set is shared by all WebSocketHandler objects. This isn't
|
||||
# optimal, but there's currently no use case for having more than one of
|
||||
# these anyway.
|
||||
clients = set()
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, msg):
|
||||
for client in cls.clients:
|
||||
client.write_message(msg)
|
||||
|
||||
def initialize(self, core):
|
||||
self.jsonrpc = make_jsonrpc_wrapper(core)
|
||||
|
||||
def open(self):
|
||||
self.set_nodelay(True)
|
||||
self.clients.add(self)
|
||||
logger.debug(
|
||||
'New WebSocket connection from %s', self.request.remote_ip)
|
||||
|
||||
def on_close(self):
|
||||
self.clients.discard(self)
|
||||
logger.debug(
|
||||
'Closed WebSocket connection from %s',
|
||||
self.request.remote_ip)
|
||||
|
||||
def on_message(self, message):
|
||||
if not message:
|
||||
return
|
||||
|
||||
logger.debug(
|
||||
'Received WebSocket message from %s: %r',
|
||||
self.request.remote_ip, message)
|
||||
|
||||
try:
|
||||
response = self.jsonrpc.handle_json(
|
||||
tornado.escape.native_str(message))
|
||||
if response and self.write_message(response):
|
||||
logger.debug(
|
||||
'Sent WebSocket message to %s: %r',
|
||||
self.request.remote_ip, response)
|
||||
except Exception as e:
|
||||
logger.error('WebSocket request error: %s', e)
|
||||
self.close()
|
||||
|
||||
|
||||
def set_mopidy_headers(request_handler):
|
||||
request_handler.set_header('Cache-Control', 'no-cache')
|
||||
request_handler.set_header(
|
||||
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
||||
|
||||
|
||||
class JsonRpcHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, core):
|
||||
self.jsonrpc = make_jsonrpc_wrapper(core)
|
||||
|
||||
def head(self):
|
||||
self.set_extra_headers()
|
||||
self.finish()
|
||||
|
||||
def post(self):
|
||||
data = self.request.body
|
||||
if not data:
|
||||
return
|
||||
|
||||
logger.debug(
|
||||
'Received RPC message from %s: %r', self.request.remote_ip, data)
|
||||
|
||||
try:
|
||||
self.set_extra_headers()
|
||||
response = self.jsonrpc.handle_json(
|
||||
tornado.escape.native_str(data))
|
||||
if response and self.write(response):
|
||||
logger.debug(
|
||||
'Sent RPC message to %s: %r',
|
||||
self.request.remote_ip, response)
|
||||
except Exception as e:
|
||||
logger.error('HTTP JSON-RPC request error:', e)
|
||||
self.write_error(500)
|
||||
|
||||
def set_extra_headers(self):
|
||||
set_mopidy_headers(self)
|
||||
self.set_header('Accept', 'application/json')
|
||||
self.set_header('Content-Type', 'application/json; utf-8')
|
||||
|
||||
|
||||
class ClientListHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, apps, statics):
|
||||
self.apps = apps
|
||||
self.statics = statics
|
||||
|
||||
def get_template_path(self):
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
def get(self):
|
||||
set_mopidy_headers(self)
|
||||
|
||||
names = set()
|
||||
for app in self.apps:
|
||||
names.add(app['name'])
|
||||
for static in self.statics:
|
||||
names.add(static['name'])
|
||||
names.discard('mopidy')
|
||||
|
||||
self.render('data/clients.html', apps=sorted(list(names)))
|
||||
|
||||
|
||||
class StaticFileHandler(tornado.web.StaticFileHandler):
|
||||
def set_extra_headers(self, path):
|
||||
set_mopidy_headers(self)
|
||||
|
||||
|
||||
class AddSlashHandler(tornado.web.RequestHandler):
|
||||
|
||||
@tornado.web.addslash
|
||||
def prepare(self):
|
||||
return super(AddSlashHandler, self).prepare()
|
||||
@ -1,93 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import cherrypy
|
||||
import ws4py.websocket
|
||||
|
||||
from mopidy import core, models
|
||||
from mopidy.utils import jsonrpc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketResource(object):
|
||||
def __init__(self, core_proxy):
|
||||
self._core = core_proxy
|
||||
inspector = jsonrpc.JsonRpcInspector(
|
||||
objects={
|
||||
'core.get_uri_schemes': core.Core.get_uri_schemes,
|
||||
'core.get_version': core.Core.get_version,
|
||||
'core.library': core.LibraryController,
|
||||
'core.playback': core.PlaybackController,
|
||||
'core.playlists': core.PlaylistsController,
|
||||
'core.tracklist': core.TracklistController,
|
||||
})
|
||||
self.jsonrpc = jsonrpc.JsonRpcWrapper(
|
||||
objects={
|
||||
'core.describe': inspector.describe,
|
||||
'core.get_uri_schemes': self._core.get_uri_schemes,
|
||||
'core.get_version': self._core.get_version,
|
||||
'core.library': self._core.library,
|
||||
'core.playback': self._core.playback,
|
||||
'core.playlists': self._core.playlists,
|
||||
'core.tracklist': self._core.tracklist,
|
||||
},
|
||||
decoders=[models.model_json_decoder],
|
||||
encoders=[models.ModelJSONEncoder])
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
logger.debug('WebSocket handler created')
|
||||
cherrypy.request.ws_handler.jsonrpc = self.jsonrpc
|
||||
|
||||
|
||||
class _WebSocket(ws4py.websocket.WebSocket):
|
||||
"""Sub-class ws4py WebSocket with better error handling."""
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
try:
|
||||
super(_WebSocket, self).send(*args, **kwargs)
|
||||
return True
|
||||
except socket.error as e:
|
||||
logger.warning('Send message failed: %s', e)
|
||||
# This isn't really correct, but its the only way to break of out
|
||||
# the loop in run and trick ws4py into cleaning up.
|
||||
self.client_terminated = self.server_terminated = True
|
||||
return False
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
try:
|
||||
super(_WebSocket, self).close(*args, **kwargs)
|
||||
except socket.error as e:
|
||||
logger.warning('Closing WebSocket failed: %s', e)
|
||||
|
||||
|
||||
class WebSocketHandler(_WebSocket):
|
||||
def opened(self):
|
||||
remote = cherrypy.request.remote
|
||||
logger.debug(
|
||||
'New WebSocket connection from %s:%d',
|
||||
remote.ip, remote.port)
|
||||
|
||||
def closed(self, code, reason=None):
|
||||
remote = cherrypy.request.remote
|
||||
logger.debug(
|
||||
'Closed WebSocket connection from %s:%d '
|
||||
'with code %s and reason %r',
|
||||
remote.ip, remote.port, code, reason)
|
||||
|
||||
def received_message(self, request):
|
||||
remote = cherrypy.request.remote
|
||||
request = str(request)
|
||||
|
||||
logger.debug(
|
||||
'Received WebSocket message from %s:%d: %r',
|
||||
remote.ip, remote.port, request)
|
||||
|
||||
response = self.jsonrpc.handle_json(request)
|
||||
if response and self.send(response):
|
||||
logger.debug(
|
||||
'Sent WebSocket message to %s:%d: %r',
|
||||
remote.ip, remote.port, response)
|
||||
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
import gobject
|
||||
|
||||
import pykka
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.utils import encoding, path
|
||||
from mopidy.local import storage
|
||||
from mopidy.local.library import LocalLibraryProvider
|
||||
from mopidy.local.playback import LocalPlaybackProvider
|
||||
from mopidy.local.playlists import LocalPlaylistsProvider
|
||||
|
||||
from .library import LocalLibraryProvider
|
||||
from .playback import LocalPlaybackProvider
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -24,7 +23,7 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
|
||||
|
||||
self.config = config
|
||||
|
||||
self.check_dirs_and_files()
|
||||
storage.check_dirs_and_files(config)
|
||||
|
||||
libraries = dict((l.name, l) for l in self.libraries)
|
||||
library_name = config['local']['library']
|
||||
@ -39,23 +38,3 @@ class LocalBackend(pykka.ThreadingActor, backend.Backend):
|
||||
self.playback = LocalPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
self.library = LocalLibraryProvider(backend=self, library=library)
|
||||
|
||||
def check_dirs_and_files(self):
|
||||
if not os.path.isdir(self.config['local']['media_dir']):
|
||||
logger.warning('Local media dir %s does not exist.' %
|
||||
self.config['local']['media_dir'])
|
||||
|
||||
try:
|
||||
path.get_or_create_dir(self.config['local']['data_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local data dir: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
# TODO: replace with data dir?
|
||||
try:
|
||||
path.get_or_create_dir(self.config['local']['playlists_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local playlists dir: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
@ -6,9 +6,9 @@ import time
|
||||
|
||||
from mopidy import commands, exceptions
|
||||
from mopidy.audio import scan
|
||||
from mopidy.local import translator
|
||||
from mopidy.utils import path
|
||||
|
||||
from . import translator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -65,59 +65,59 @@ class ScanCommand(commands.Command):
|
||||
scan_timeout = config['local']['scan_timeout']
|
||||
flush_threshold = config['local']['scan_flush_threshold']
|
||||
excluded_file_extensions = config['local']['excluded_file_extensions']
|
||||
excluded_file_extensions = set(
|
||||
file_ext.lower() for file_ext in excluded_file_extensions)
|
||||
excluded_file_extensions = tuple(
|
||||
bytes(file_ext.lower()) for file_ext in excluded_file_extensions)
|
||||
|
||||
library = _get_library(args, config)
|
||||
|
||||
uri_path_mapping = {}
|
||||
uris_in_library = set()
|
||||
uris_to_update = set()
|
||||
uris_to_remove = set()
|
||||
|
||||
file_mtimes = path.find_mtimes(media_dir)
|
||||
logger.info('Found %d files in media_dir.', len(file_mtimes))
|
||||
|
||||
num_tracks = library.load()
|
||||
logger.info('Checking %d tracks from library.', num_tracks)
|
||||
|
||||
for track in library.begin():
|
||||
uri_path_mapping[track.uri] = translator.local_track_uri_to_path(
|
||||
track.uri, media_dir)
|
||||
try:
|
||||
stat = os.stat(uri_path_mapping[track.uri])
|
||||
if int(stat.st_mtime) > track.last_modified:
|
||||
uris_to_update.add(track.uri)
|
||||
uris_in_library.add(track.uri)
|
||||
except OSError:
|
||||
abspath = translator.local_track_uri_to_path(track.uri, media_dir)
|
||||
mtime = file_mtimes.pop(abspath, None)
|
||||
if mtime is None:
|
||||
logger.debug('Missing file %s', track.uri)
|
||||
uris_to_remove.add(track.uri)
|
||||
elif mtime > track.last_modified:
|
||||
uris_in_library.add(track.uri)
|
||||
|
||||
logger.info('Removing %d missing tracks.', len(uris_to_remove))
|
||||
for uri in uris_to_remove:
|
||||
library.remove(uri)
|
||||
|
||||
logger.info('Checking %s for unknown tracks.', media_dir)
|
||||
for relpath in path.find_files(media_dir):
|
||||
for abspath in file_mtimes:
|
||||
relpath = os.path.relpath(abspath, media_dir)
|
||||
uri = translator.path_to_local_track_uri(relpath)
|
||||
file_extension = os.path.splitext(relpath)[1]
|
||||
|
||||
if file_extension.lower() in excluded_file_extensions:
|
||||
if relpath.lower().endswith(excluded_file_extensions):
|
||||
logger.debug('Skipped %s: File extension excluded.', uri)
|
||||
continue
|
||||
|
||||
if uri not in uris_in_library:
|
||||
uris_to_update.add(uri)
|
||||
uri_path_mapping[uri] = os.path.join(media_dir, relpath)
|
||||
uris_to_update.add(uri)
|
||||
|
||||
logger.info('Found %d unknown tracks.', len(uris_to_update))
|
||||
logger.info(
|
||||
'Found %d tracks which need to be updated.', len(uris_to_update))
|
||||
logger.info('Scanning...')
|
||||
|
||||
uris_to_update = sorted(uris_to_update)[:args.limit]
|
||||
uris_to_update = sorted(uris_to_update, key=lambda v: v.lower())
|
||||
uris_to_update = uris_to_update[:args.limit]
|
||||
|
||||
scanner = scan.Scanner(scan_timeout)
|
||||
progress = _Progress(flush_threshold, len(uris_to_update))
|
||||
|
||||
for uri in uris_to_update:
|
||||
try:
|
||||
data = scanner.scan(path.path_to_uri(uri_path_mapping[uri]))
|
||||
relpath = translator.local_track_uri_to_path(uri, media_dir)
|
||||
file_uri = path.path_to_uri(os.path.join(media_dir, relpath))
|
||||
data = scanner.scan(file_uri)
|
||||
track = scan.audio_data_to_track(data).copy(uri=uri)
|
||||
library.add(track)
|
||||
logger.debug('Added %s', track.uri)
|
||||
|
||||
@ -7,6 +7,7 @@ playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
|
||||
scan_timeout = 1000
|
||||
scan_flush_threshold = 1000
|
||||
excluded_file_extensions =
|
||||
.directory
|
||||
.html
|
||||
.jpeg
|
||||
.jpg
|
||||
|
||||
@ -12,18 +12,29 @@ import time
|
||||
|
||||
import mopidy
|
||||
from mopidy import local, models
|
||||
from mopidy.local import search, translator
|
||||
from mopidy.local import search, storage, translator
|
||||
from mopidy.utils import encoding
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: move to load and dump in models?
|
||||
def load_library(json_file):
|
||||
if not os.path.isfile(json_file):
|
||||
logger.info(
|
||||
'No local library metadata cache found at %s. Please run '
|
||||
'`mopidy local scan` to index your local music library. '
|
||||
'If you do not have a local music collection, you can disable the '
|
||||
'local backend to hide this message.',
|
||||
json_file)
|
||||
return {}
|
||||
try:
|
||||
with gzip.open(json_file, 'rb') as fp:
|
||||
return json.load(fp, object_hook=models.model_json_decoder)
|
||||
except (IOError, ValueError) as e:
|
||||
logger.warning('Loading JSON local library failed: %s', e)
|
||||
except (IOError, ValueError) as error:
|
||||
logger.warning(
|
||||
'Loading JSON local library failed: %s',
|
||||
encoding.locale_decode(error))
|
||||
return {}
|
||||
|
||||
|
||||
@ -121,6 +132,8 @@ class JsonLibrary(local.Library):
|
||||
self._json_file = os.path.join(
|
||||
config['local']['data_dir'], b'library.json.gz')
|
||||
|
||||
storage.check_dirs_and_files(config)
|
||||
|
||||
def browse(self, uri):
|
||||
if not self._browse_cache:
|
||||
return []
|
||||
|
||||
@ -3,8 +3,8 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.local import translator
|
||||
|
||||
from . import translator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
30
mopidy/local/storage.py
Normal file
@ -0,0 +1,30 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_dirs_and_files(config):
|
||||
if not os.path.isdir(config['local']['media_dir']):
|
||||
logger.warning(
|
||||
'Local media dir %s does not exist.' %
|
||||
config['local']['media_dir'])
|
||||
|
||||
try:
|
||||
path.get_or_create_dir(config['local']['data_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local data dir: %s',
|
||||
encoding.locale_decode(error))
|
||||
|
||||
# TODO: replace with data dir?
|
||||
try:
|
||||
path.get_or_create_dir(config['local']['playlists_dir'])
|
||||
except EnvironmentError as error:
|
||||
logger.warning(
|
||||
'Could not create local playlists dir: %s',
|
||||
encoding.locale_decode(error))
|
||||