Release v0.18.0
1
.gitignore
vendored
@ -16,3 +16,4 @@ node_modules/
|
||||
nosetests.xml
|
||||
*~
|
||||
*.orig
|
||||
js/test/lib/
|
||||
|
||||
4
.mailmap
@ -10,3 +10,7 @@ Alexandre Petitjean <alpetitjean@gmail.com>
|
||||
Alexandre Petitjean <alpetitjean@gmail.com> <alexandre.petitjean@lne.fr>
|
||||
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>
|
||||
Luke Giuliani <luke@giuliani.com.au>
|
||||
Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
|
||||
6
AUTHORS
@ -29,3 +29,9 @@
|
||||
- Javier Domingo <javierdo1@gmail.com>
|
||||
- Lasse Bigum <lasse@bigum.org>
|
||||
- David Eisner <david.eisner@oriel.oxon.org>
|
||||
- Pål Ruud <ruudud@gmail.com>
|
||||
- Thomas Kemmer <tkemmer@computer.org>
|
||||
- Paul Connolley <paul.connolley@gmail.com>
|
||||
- Luke Giuliani <luke@giuliani.com.au>
|
||||
- Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
- Simon de Bakker <simon@simbits.nl>
|
||||
|
||||
16
MANIFEST.in
@ -1,13 +1,23 @@
|
||||
include *.py
|
||||
include *.rst
|
||||
include .coveragerc
|
||||
include .mailmap
|
||||
include .travis.yml
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include data/mopidy.desktop
|
||||
|
||||
recursive-include data *
|
||||
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
|
||||
recursive-include js *
|
||||
prune js/node_modules
|
||||
prune js/test/lib
|
||||
|
||||
recursive-include mopidy *.conf
|
||||
recursive-include mopidy/frontends/http/data *
|
||||
recursive-include requirements *
|
||||
recursive-include mopidy/http/data *
|
||||
|
||||
recursive-include tests *.py
|
||||
recursive-include tests/data *
|
||||
|
||||
@ -26,11 +26,11 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
|
||||
|
||||
.. image:: https://pypip.in/v/Mopidy/badge.png
|
||||
:target: https://crate.io/packages/Mopidy/
|
||||
:target: https://pypi.python.org/pypi/Mopidy/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
.. image:: https://pypip.in/d/Mopidy/badge.png
|
||||
:target: https://crate.io/packages/Mopidy/
|
||||
:target: https://pypi.python.org/pypi/Mopidy/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
|
||||
@ -40,7 +40,3 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
.. image:: https://coveralls.io/repos/mopidy/mopidy/badge.png?branch=develop
|
||||
:target: https://coveralls.io/r/mopidy/mopidy?branch=develop
|
||||
:alt: Test coverage
|
||||
|
||||
.. image:: https://sourcegraph.com/api/repos/github.com/mopidy/mopidy/counters/views-24h.png
|
||||
:target: https://sourcegraph.com/github.com/mopidy/mopidy
|
||||
:alt: Mopidy stats
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=Mopidy Music Server
|
||||
Comment=MPD music server with Spotify support
|
||||
Name=Mopidy
|
||||
Comment=Music server with support for MPD and HTTP clients
|
||||
Icon=audio-x-generic
|
||||
TryExec=mopidy
|
||||
Exec=mopidy
|
||||
|
||||
BIN
docs/_static/woutervanwijk-mopidy-webclient.png
vendored
|
Before Width: | Height: | Size: 47 KiB |
@ -4,46 +4,92 @@
|
||||
Backend API
|
||||
***********
|
||||
|
||||
.. module:: mopidy.backends.base
|
||||
.. module:: mopidy.backend
|
||||
:synopsis: The API implemented by backends
|
||||
|
||||
The backend API is the interface that must be implemented when you create a
|
||||
backend. If you are working on a frontend and need to access the backend, see
|
||||
the :ref:`core-api`.
|
||||
backend. If you are working on a frontend and need to access the backends, see
|
||||
the :ref:`core-api` instead.
|
||||
|
||||
|
||||
URIs and routing of requests to the backend
|
||||
===========================================
|
||||
|
||||
When Mopidy's core layer is processing a client request, it routes the request
|
||||
to one or more appropriate backends based on the URIs of the objects the
|
||||
request touches on. The objects' URIs are compared with the backends'
|
||||
:attr:`~mopidy.backend.Backend.uri_schemes` to select the relevant backends.
|
||||
|
||||
An often used pattern when implementing Mopidy backends is to create your own
|
||||
URI scheme which you use for all tracks, playlists, etc. related to your
|
||||
backend. In most cases the Mopidy URI is translated to an actual URI that
|
||||
GStreamer knows how to play right before playback. For example:
|
||||
|
||||
- Spotify already has its own URI scheme (``spotify:track:...``,
|
||||
``spotify:playlist:...``, etc.) used throughout their applications, and thus
|
||||
Mopidy-Spotify simply uses the same URI scheme. Playback is handled by
|
||||
pushing raw audio data into a GStreamer ``appsrc`` element.
|
||||
|
||||
- Mopidy-SoundCloud created it's own URI scheme, after the model of Spotify,
|
||||
and use URIs of the following forms: ``soundcloud:search``,
|
||||
``soundcloud:user-...``, ``soundcloud:exp-...``, and ``soundcloud:set-...``.
|
||||
Playback is handled by converting the custom ``soundcloud:..`` URIs to
|
||||
``http://`` URIs immediately before they are passed on to GStreamer for
|
||||
playback.
|
||||
|
||||
- Mopidy differentiates between ``file://...`` URIs handled by
|
||||
:ref:`ext-stream` and ``local:...`` URIs handled by :ref:`ext-local`.
|
||||
:ref:`ext-stream` can play ``file://...`` URIs pointing to tracks and
|
||||
playlists located anywhere on your system, but it doesn't know a thing about
|
||||
the object before you play it. On the other hand, :ref:`ext-local` scans a
|
||||
predefined :confval:`local/media_dir` to build a meta data library of all
|
||||
known tracks. It is thus limited to playing tracks residing in the media
|
||||
library, but can provide additional features like directory browsing and
|
||||
search. In other words, we have two different ways of playing local music,
|
||||
handled by two different backends, and have thus created two different URI
|
||||
schemes to separate their handling. The ``local:...`` URIs are converted to
|
||||
``file://...`` URIs immediately before they are passed on to GStreamer for
|
||||
playback.
|
||||
|
||||
If there isn't an existing URI scheme that fits for your backend's purpose,
|
||||
you should create your own, and name it after your extension's
|
||||
:attr:`~mopidy.ext.Extension.ext_name`. Care should be taken not to conflict
|
||||
with already in use URI schemes. It is also recommended to design the format
|
||||
such that tracks, playlists and other entities can be distinguished easily.
|
||||
|
||||
|
||||
Backend class
|
||||
=============
|
||||
|
||||
.. autoclass:: mopidy.backends.base.Backend
|
||||
.. autoclass:: mopidy.backend.Backend
|
||||
:members:
|
||||
|
||||
|
||||
Playback provider
|
||||
=================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BasePlaybackProvider
|
||||
.. autoclass:: mopidy.backend.PlaybackProvider
|
||||
:members:
|
||||
|
||||
|
||||
Playlists provider
|
||||
==================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BasePlaylistsProvider
|
||||
.. autoclass:: mopidy.backend.PlaylistsProvider
|
||||
:members:
|
||||
|
||||
|
||||
Library provider
|
||||
================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseLibraryProvider
|
||||
.. autoclass:: mopidy.backend.LibraryProvider
|
||||
:members:
|
||||
|
||||
|
||||
Backend listener
|
||||
================
|
||||
|
||||
.. autoclass:: mopidy.backends.listener.BackendListener
|
||||
.. autoclass:: mopidy.backend.BackendListener
|
||||
:members:
|
||||
|
||||
|
||||
@ -52,6 +98,22 @@ Backend listener
|
||||
Backend implementations
|
||||
=======================
|
||||
|
||||
* :mod:`mopidy.backends.dummy`
|
||||
* :mod:`mopidy.backends.local`
|
||||
* :mod:`mopidy.backends.stream`
|
||||
- `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>`_
|
||||
|
||||
@ -7,10 +7,12 @@ Core API
|
||||
.. module:: mopidy.core
|
||||
:synopsis: Core API for use by frontends
|
||||
|
||||
|
||||
The core API is the interface that is used by frontends like
|
||||
:mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the
|
||||
backends.
|
||||
:mod:`mopidy.http` and :mod:`mopidy.mpd`. The core layer is inbetween the
|
||||
frontends and the backends.
|
||||
|
||||
.. autoclass:: mopidy.core.Core
|
||||
:members:
|
||||
|
||||
|
||||
Playback controller
|
||||
|
||||
@ -47,5 +47,12 @@ The following requirements applies to any frontend implementation:
|
||||
Frontend implementations
|
||||
========================
|
||||
|
||||
* :mod:`mopidy.frontends.http`
|
||||
* :mod:`mopidy.frontends.mpd`
|
||||
- :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>`_
|
||||
|
||||
@ -113,8 +113,8 @@ HTML file:
|
||||
If you don't use Mopidy to host your web client, you can find the JS files in
|
||||
the Git repo at:
|
||||
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
- ``mopidy/http/data/mopidy.js``
|
||||
- ``mopidy/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Getting the library for Node.js use
|
||||
@ -129,7 +129,7 @@ After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Getting the library for development on the library
|
||||
|
||||
@ -4,6 +4,15 @@
|
||||
API reference
|
||||
*************
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Only APIs documented here are public and open for use by Mopidy
|
||||
extensions. We will change these APIs, but will keep the changelog up to
|
||||
date with all breaking changes.
|
||||
|
||||
From Mopidy 1.0 and onwards, we intend to keep these APIs far more stable.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
@ -16,4 +25,5 @@ API reference
|
||||
commands
|
||||
ext
|
||||
config
|
||||
zeroconf
|
||||
http
|
||||
|
||||
11
docs/api/zeroconf.rst
Normal file
@ -0,0 +1,11 @@
|
||||
.. _zeroconf-api:
|
||||
|
||||
************
|
||||
Zeroconf API
|
||||
************
|
||||
|
||||
.. module:: mopidy.zeroconf
|
||||
:synopsis: Helper for publishing of services on Zeroconf
|
||||
|
||||
.. autoclass:: Zeroconf
|
||||
:members:
|
||||
@ -4,7 +4,7 @@
|
||||
Authors
|
||||
*******
|
||||
|
||||
Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. Mopidy is
|
||||
Mopidy is copyright 2009-2014 Stein Magnus Jodal and contributors. Mopidy is
|
||||
licensed under the `Apache License, Version 2.0
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>`_.
|
||||
|
||||
|
||||
@ -4,6 +4,172 @@ Changelog
|
||||
|
||||
This changelog is used to track all major changes to Mopidy.
|
||||
|
||||
v0.18.0 (2014-01-19)
|
||||
====================
|
||||
|
||||
The focus of 0.18 have been on two fronts: the local library and browsing.
|
||||
|
||||
First, the local library's old tag cache file used for storing the track
|
||||
metadata scanned from your music collection has been replaced with a far
|
||||
simpler implementation using JSON as the storage format. At the same time, the
|
||||
local library have been made replaceable by extensions, so you can now create
|
||||
extensions that use your favorite database to store the metadata.
|
||||
|
||||
Second, we've finally implemented the long awaited "file system" browsing
|
||||
feature that you know from MPD. It is supported by both the MPD frontend and
|
||||
the local and Spotify backends. It is also used by the new Mopidy-Dirble
|
||||
extension to provide you with a directory of Internet radio stations from all
|
||||
over the world.
|
||||
|
||||
Since the release of 0.17, we've closed or merged 49 issues and pull requests
|
||||
through about 285 commits by :ref:`11 people <authors>`, including six new
|
||||
guys. Thanks to everyone that has contributed!
|
||||
|
||||
**Core API**
|
||||
|
||||
- Add :meth:`mopidy.core.Core.version` for HTTP clients to manage compatibility
|
||||
between API versions. (Fixes: :issue:`597`)
|
||||
|
||||
- Add :class:`mopidy.models.Ref` class for use as a lightweight reference to
|
||||
other model types, containing just an URI, a name, and an object type. It is
|
||||
barely used for now, but its use will be extended over time.
|
||||
|
||||
- Add :meth:`mopidy.core.LibraryController.browse` method for browsing a
|
||||
virtual file system of tracks. Backends can implement support for this by
|
||||
implementing :meth:`mopidy.backend.LibraryProvider.browse`.
|
||||
|
||||
- Events emitted on play/stop, pause/resume, next/previous and on end of track
|
||||
has been cleaned up to work consistently. See the message of
|
||||
:commit:`1d108752f6` for the full details. (Fixes: :issue:`629`)
|
||||
|
||||
**Backend API**
|
||||
|
||||
- Move the backend API classes from :mod:`mopidy.backends.base` to
|
||||
:mod:`mopidy.backend` and remove the ``Base`` prefix from the class names:
|
||||
|
||||
- From :class:`mopidy.backends.base.Backend`
|
||||
to :class:`mopidy.backend.Backend`
|
||||
|
||||
- From :class:`mopidy.backends.base.BaseLibraryProvider`
|
||||
to :class:`mopidy.backend.LibraryProvider`
|
||||
|
||||
- From :class:`mopidy.backends.base.BasePlaybackProvider`
|
||||
to :class:`mopidy.backend.PlaybackProvider`
|
||||
|
||||
- From :class:`mopidy.backends.base.BasePlaylistsProvider`
|
||||
to :class:`mopidy.backend.PlaylistsProvider`
|
||||
|
||||
- From :class:`mopidy.backends.listener.BackendListener`
|
||||
to :class:`mopidy.backend.BackendListener`
|
||||
|
||||
Imports from the old locations still works, but are deprecated.
|
||||
|
||||
- Add :meth:`mopidy.backend.LibraryProvider.browse`, which can be implemented
|
||||
by backends that wants to expose directories of tracks in Mopidy's virtual
|
||||
file system.
|
||||
|
||||
**Frontend API**
|
||||
|
||||
- The dummy backend used for testing many frontends have moved from
|
||||
:mod:`mopidy.backends.dummy` to :mod:`mopidy.backend.dummy`.
|
||||
|
||||
**Commands**
|
||||
|
||||
- Reduce amount of logging from dependencies when using :option:`mopidy -v`.
|
||||
(Fixes: :issue:`593`)
|
||||
|
||||
- Add support for additional logging verbosity levels with ``mopidy -vv`` and
|
||||
``mopidy -vvv`` which increases the amount of logging from dependencies.
|
||||
(Fixes: :issue:`593`)
|
||||
|
||||
**Configuration**
|
||||
|
||||
- The default for the :option:`mopidy --config` option has been updated to
|
||||
include ``$XDG_CONFIG_DIRS`` in addition to ``$XDG_CONFIG_DIR``. (Fixes
|
||||
:issue:`431`)
|
||||
|
||||
- Added support for deprecating config values in order to allow for graceful
|
||||
removal of the no longer used config value :confval:`local/tag_cache_file`.
|
||||
|
||||
**Extension support**
|
||||
|
||||
- Switched to using a registry model for classes provided by extension. This
|
||||
allows extensions to be extended by other extensions, as needed by for
|
||||
example pluggable libraries for the local backend. See
|
||||
:class:`mopidy.ext.Registry` for details. (Fixes :issue:`601`)
|
||||
|
||||
- Added the new method :meth:`mopidy.ext.Extension.setup`. This method
|
||||
replaces the now deprecated
|
||||
:meth:`~mopidy.ext.Extension.get_backend_classes`,
|
||||
:meth:`~mopidy.ext.Extension.get_frontend_classes`, and
|
||||
:meth:`~mopidy.ext.Extension.register_gstreamer_elements`.
|
||||
|
||||
**Audio**
|
||||
|
||||
- Added :confval:`audio/mixer_volume` to set the initial volume of mixers.
|
||||
This is especially useful for setting the software mixer volume to something
|
||||
else than the default 100%. (Fixes: :issue:`633`)
|
||||
|
||||
**Local backend**
|
||||
|
||||
.. note::
|
||||
|
||||
After upgrading to Mopidy 0.18 you must run ``mopidy local scan`` to
|
||||
reindex your local music collection. This is due to the change of storage
|
||||
format.
|
||||
|
||||
- Added support for browsing local directories in Mopidy's virtual file system.
|
||||
|
||||
- Finished the work on creating pluggable libraries. Users can now
|
||||
reconfigure Mopidy to use alternate library providers of their choosing for
|
||||
local files. (Fixes issue :issue:`44`, partially resolves :issue:`397`, and
|
||||
causes a temporary regression of :issue:`527`.)
|
||||
|
||||
- Switched default local library provider from a "tag cache" file that closely
|
||||
resembled the one used by the original MPD server to a compressed JSON file.
|
||||
This greatly simplifies our library code and reuses our existing model
|
||||
serialization code, as used by the HTTP API and web clients.
|
||||
|
||||
- Removed our outdated and bug-ridden "tag cache" local library implementation.
|
||||
|
||||
- Added the config value :confval:`local/library` to select which library to
|
||||
use. It defaults to ``json``, which is the only local library bundled with
|
||||
Mopidy.
|
||||
|
||||
- Added the config value :confval:`local/data_dir` to have a common config for
|
||||
where to store local library data. This is intended to avoid every single
|
||||
local library provider having to have it's own config value for this.
|
||||
|
||||
- Added the config value :confval:`local/scan_flush_threshold` to control how
|
||||
often to tell local libraries to store changes when scanning local music.
|
||||
|
||||
**Streaming backend**
|
||||
|
||||
- Add live lookup of URI metadata. (Fixes :issue:`540`)
|
||||
|
||||
- Add support for extended M3U playlist, meaning that basic track metadata
|
||||
stored in playlists will be used by Mopidy.
|
||||
|
||||
**HTTP frontend**
|
||||
|
||||
- Upgrade Mopidy.js dependencies and add support for using Mopidy.js with
|
||||
Browserify. This version has been released to npm as Mopidy.js v0.2.0.
|
||||
(Fixes: :issue:`609`)
|
||||
|
||||
**MPD frontend**
|
||||
|
||||
- Make the ``lsinfo``, ``listall``, and ``listallinfo`` commands support
|
||||
browsing of Mopidy's virtual file system. (Fixes: :issue:`145`)
|
||||
|
||||
- Empty commands now return a ``ACK [5@0] {} No command given`` error instead
|
||||
of ``OK``. This is consistent with the original MPD server implementation.
|
||||
|
||||
**Internal changes**
|
||||
|
||||
- Events from the audio actor, backends, and core actor are now emitted
|
||||
asyncronously through the GObject event loop. This should resolve the issue
|
||||
that has blocked the merge of the EOT-vs-EOS fix for a long time.
|
||||
|
||||
|
||||
v0.17.0 (2013-11-23)
|
||||
====================
|
||||
@ -370,7 +536,7 @@ A release with a number of small and medium fixes, with no specific focus.
|
||||
objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the
|
||||
``tlid`` field. (Fixes: :issue:`501`)
|
||||
|
||||
- Upgrade Mopidy.js dependencies. This version has been released to NPM as
|
||||
- Upgrade Mopidy.js dependencies. This version has been released to npm as
|
||||
Mopidy.js v0.1.1.
|
||||
|
||||
**Extension support**
|
||||
@ -616,9 +782,9 @@ throughout Mopidy.
|
||||
**Stream backend**
|
||||
|
||||
We've added a new backend for playing audio streams, the :mod:`stream backend
|
||||
<mopidy.backends.stream>`. It is activated by default. The stream backend
|
||||
supports the intersection of what your GStreamer installation supports and what
|
||||
protocols are included in the :attr:`mopidy.settings.STREAM_PROTOCOLS` setting.
|
||||
<mopidy.stream>`. It is activated by default. The stream backend supports the
|
||||
intersection of what your GStreamer installation supports and what protocols
|
||||
are included in the :attr:`mopidy.settings.STREAM_PROTOCOLS` setting.
|
||||
|
||||
Current limitations:
|
||||
|
||||
@ -1657,8 +1823,8 @@ to this problem.
|
||||
- Local backend:
|
||||
|
||||
- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without
|
||||
any help from the original MPD server. See :ref:`generating-a-tag-cache`
|
||||
for instructions on how to use it.
|
||||
any help from the original MPD server. See
|
||||
:ref:`generating-a-local-library` for instructions on how to use it.
|
||||
|
||||
- Fix support for UTF-8 encoding in tag caches.
|
||||
|
||||
@ -1714,7 +1880,7 @@ to this problem.
|
||||
|
||||
- Packaging and distribution:
|
||||
|
||||
- Setup APT repository and crate Debian packages of Mopidy. See
|
||||
- Setup APT repository and create Debian packages of Mopidy. See
|
||||
:ref:`installation` for instructions for how to install Mopidy, including
|
||||
all dependencies, from APT.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 519 KiB |
@ -18,34 +18,62 @@ See :ref:`http-api` for details on how to build your own web client.
|
||||
woutervanwijk/Mopidy-Webclient
|
||||
==============================
|
||||
|
||||
.. image:: /_static/woutervanwijk-mopidy-webclient.png
|
||||
:width: 382
|
||||
:height: 621
|
||||
.. image:: woutervanwijk-mopidy-webclient.png
|
||||
:width: 1275
|
||||
:height: 600
|
||||
|
||||
The first web client for Mopidy is still under development, but is already very
|
||||
usable. It targets both desktop and mobile browsers.
|
||||
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.
|
||||
|
||||
The web client used for the `Pi Musicbox
|
||||
<http://www.woutervanwijk.nl/pimusicbox/>`_ is also available for other users
|
||||
of Mopidy. See https://github.com/woutervanwijk/Mopidy-WebClient for details.
|
||||
With Mopidy Browser Client, you can play your music on your computer (or
|
||||
Rapsberry Pi) and remotely control it from a computer, phone, tablet,
|
||||
laptop. From your couch.
|
||||
|
||||
-- https://github.com/woutervanwijk/Mopidy-WebClient
|
||||
|
||||
|
||||
Mopidy Lux
|
||||
==========
|
||||
|
||||
.. image:: /_static/dz0ny-mopidy-lux.png
|
||||
.. image:: dz0ny-mopidy-lux.png
|
||||
:width: 1000
|
||||
:height: 645
|
||||
|
||||
New web client developed by Janez Troha. See
|
||||
https://github.com/dz0ny/mopidy-lux for details.
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
JukePi
|
||||
======
|
||||
|
||||
New web client developed by Meantime IT in the UK for their office jukebox. See
|
||||
https://github.com/meantimeit/jukepi for details.
|
||||
A Mopidy web client made with Backbone.js by Meantime IT in the UK for their
|
||||
office jukebox.
|
||||
|
||||
JukePi is a web client for the Mopidy music server. Mopidy empowers you to
|
||||
create a custom music server that can connect to Spotify, play local mp3s
|
||||
and more.
|
||||
|
||||
-- https://github.com/meantimeit/jukepi
|
||||
|
||||
|
||||
Other web clients
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
*******
|
||||
Clients
|
||||
*******
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
**
|
||||
BIN
docs/clients/martijnboland-moped.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -51,7 +51,7 @@ ncmpcpp
|
||||
A console client that works well with Mopidy, and is regularly used by Mopidy
|
||||
developers.
|
||||
|
||||
.. image:: /_static/mpd-client-ncmpcpp.png
|
||||
.. image:: mpd-client-ncmpcpp.png
|
||||
:width: 575
|
||||
:height: 426
|
||||
|
||||
@ -84,7 +84,7 @@ GMPC
|
||||
`GMPC <http://gmpc.wikia.com>`_ is a graphical MPD client (GTK+) which works
|
||||
well with Mopidy.
|
||||
|
||||
.. image:: /_static/mpd-client-gmpc.png
|
||||
.. image:: mpd-client-gmpc.png
|
||||
:width: 1000
|
||||
:height: 565
|
||||
|
||||
@ -101,7 +101,7 @@ Sonata
|
||||
`Sonata <http://sonata.berlios.de/>`_ is a graphical MPD client (GTK+).
|
||||
It generally works well with Mopidy, except for search.
|
||||
|
||||
.. image:: /_static/mpd-client-sonata.png
|
||||
.. image:: mpd-client-sonata.png
|
||||
:width: 475
|
||||
:height: 424
|
||||
|
||||
@ -140,7 +140,7 @@ Test date:
|
||||
Tested version:
|
||||
1.03.1 (released 2012-10-16)
|
||||
|
||||
.. image:: /_static/mpd-client-mpdroid.jpg
|
||||
.. image:: mpd-client-mpdroid.jpg
|
||||
:width: 288
|
||||
:height: 512
|
||||
|
||||
@ -269,7 +269,7 @@ Test date:
|
||||
Tested version:
|
||||
1.7.1
|
||||
|
||||
.. image:: /_static/mpd-client-mpod.jpg
|
||||
.. image:: mpd-client-mpod.jpg
|
||||
:width: 320
|
||||
:height: 480
|
||||
|
||||
@ -297,7 +297,7 @@ Test date:
|
||||
Tested version:
|
||||
1.7.1
|
||||
|
||||
.. image:: /_static/mpd-client-mpad.jpg
|
||||
.. image:: mpd-client-mpad.jpg
|
||||
:width: 480
|
||||
:height: 360
|
||||
|
||||
@ -332,7 +332,7 @@ other web clients, see :ref:`http-clients`.
|
||||
Rompr
|
||||
-----
|
||||
|
||||
.. image:: /_static/rompr.png
|
||||
.. image:: rompr.png
|
||||
:width: 557
|
||||
:height: 600
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ sound menu in Ubuntu since 10.10 or 11.04. By default, it only includes the
|
||||
Rhytmbox music player, but many other players can integrate with the sound
|
||||
menu, including the official Spotify player and Mopidy.
|
||||
|
||||
.. image:: /_static/ubuntu-sound-menu.png
|
||||
.. image:: ubuntu-sound-menu.png
|
||||
:height: 480
|
||||
:width: 955
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
BIN
docs/clients/woutervanwijk-mopidy-webclient.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
@ -43,7 +43,7 @@ Options
|
||||
|
||||
.. cmdoption:: --verbose, -v
|
||||
|
||||
Show more output: debug level and higher.
|
||||
Show more output. Repeat up to 3 times for even more.
|
||||
|
||||
.. cmdoption:: --save-debug-log
|
||||
|
||||
@ -83,6 +83,10 @@ 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
|
||||
|
||||
Clear local media files from the local library.
|
||||
|
||||
.. cmdoption:: local scan
|
||||
|
||||
Scan local media files present in your library.
|
||||
|
||||
@ -28,6 +28,12 @@ class Mock(object):
|
||||
def __getattr__(self, name):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif name == 'get_system_config_dirs':
|
||||
# glib.get_system_config_dirs()
|
||||
return tuple
|
||||
elif name == 'get_user_config_dir':
|
||||
# glib.get_user_config_dir()
|
||||
return str
|
||||
elif (name[0] == name[0].upper()
|
||||
# gst.interfaces.MIXER_TRACK_*
|
||||
and not name.startswith('MIXER_TRACK_')
|
||||
@ -91,7 +97,7 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
project = 'Mopidy'
|
||||
copyright = '2009-2013, Stein Magnus Jodal and contributors'
|
||||
copyright = '2009-2014, Stein Magnus Jodal and contributors'
|
||||
|
||||
from mopidy.utils.versioning import get_version
|
||||
release = get_version()
|
||||
@ -155,6 +161,7 @@ man_pages = [
|
||||
|
||||
extlinks = {
|
||||
'issue': ('https://github.com/mopidy/mopidy/issues/%s', '#'),
|
||||
'commit': ('https://github.com/mopidy/mopidy/commit/%s', 'commit '),
|
||||
'mpris': (
|
||||
'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'),
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
.. _config:
|
||||
|
||||
*************
|
||||
Configuration
|
||||
*************
|
||||
@ -78,6 +80,15 @@ Core configuration values
|
||||
|
||||
Setting the config value to blank turns off volume control.
|
||||
|
||||
.. confval:: audio/mixer_volume
|
||||
|
||||
Initial volume for the audio mixer.
|
||||
|
||||
Expects an integer between 0 and 100.
|
||||
|
||||
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.
|
||||
@ -220,7 +231,7 @@ Streaming through SHOUTcast/Icecast
|
||||
Currently, Mopidy does not handle end-of-track vs end-of-stream signalling
|
||||
in GStreamer correctly. This causes the SHOUTcast stream to be disconnected
|
||||
at the end of each track, rendering it quite useless. For further details,
|
||||
see :issue:`492`.
|
||||
see :issue:`492`. You can also try the workaround_ mentioned below.
|
||||
|
||||
If you want to play the audio on another computer than the one running Mopidy,
|
||||
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
|
||||
@ -236,17 +247,37 @@ 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, to set the username and password, use:
|
||||
example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[audio]
|
||||
output = lame ! shout2send username="alice" password="secret"
|
||||
output = lame ! shout2send username="alice" password="secret" mount="mopidy"
|
||||
|
||||
Other advanced setups are also possible for outputs. Basically, anything you
|
||||
can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
:confval:`audio/output`.
|
||||
|
||||
.. _workaround:
|
||||
|
||||
**Workaround for end-of-track issues - fallback streams**
|
||||
|
||||
By using a *fallback stream* playing silence, you can somewhat mitigate the
|
||||
signalling issues.
|
||||
|
||||
Example Icecast configuration:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<mount>
|
||||
<mount-name>/mopidy</mount-name>
|
||||
<fallback-mount>/silence.mp3</fallback-mount>
|
||||
<fallback-override>1</fallback-override>
|
||||
</mount>
|
||||
|
||||
The ``silence.mp3`` file needs to be placed in the directory defined by
|
||||
``<webroot>...</webroot>``.
|
||||
|
||||
|
||||
New configuration values
|
||||
------------------------
|
||||
|
||||
@ -85,7 +85,7 @@ Mopidy to come with tests.
|
||||
#. To run tests, you need a couple of dependencies. They can be installed using
|
||||
``pip``::
|
||||
|
||||
pip install -r requirements/tests.txt
|
||||
pip install --upgrade coverage flake8 mock nose
|
||||
|
||||
#. Then, to run all tests, go to the project directory and run::
|
||||
|
||||
|
||||
@ -27,51 +27,6 @@ code. So, if you're out of work, the code coverage and flake8 data at the CI
|
||||
server should give you a place to start.
|
||||
|
||||
|
||||
Protocol debugger
|
||||
=================
|
||||
|
||||
Since the main interface provided to Mopidy is through the MPD protocol, it is
|
||||
crucial that we try and stay in sync with protocol developments. In an attempt
|
||||
to make it easier to debug differences Mopidy and MPD protocol handling we have
|
||||
created ``tools/debug-proxy.py``.
|
||||
|
||||
This tool is proxy that sits in front of two MPD protocol aware servers and
|
||||
sends all requests to both, returning the primary response to the client and
|
||||
then printing any diff in the two responses.
|
||||
|
||||
Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time
|
||||
of writing. See :option:`tools/debug-proxy.py --help` for available options.
|
||||
Sample session::
|
||||
|
||||
[127.0.0.1]:59714
|
||||
listallinfo
|
||||
--- Reference response
|
||||
+++ Actual response
|
||||
@@ -1,16 +1,1 @@
|
||||
-file: uri1
|
||||
-Time: 4
|
||||
-Artist: artist1
|
||||
-Title: track1
|
||||
-Album: album1
|
||||
-file: uri2
|
||||
-Time: 4
|
||||
-Artist: artist2
|
||||
-Title: track2
|
||||
-Album: album2
|
||||
-file: uri3
|
||||
-Time: 4
|
||||
-Artist: artist3
|
||||
-Title: track3
|
||||
-Album: album3
|
||||
-OK
|
||||
+ACK [2@0] {listallinfo} incorrect arguments
|
||||
|
||||
To ensure that Mopidy and MPD have comparable state it is suggested you setup
|
||||
both to use ``tests/data/advanced_tag_cache`` for their tag cache and
|
||||
``tests/data/scanner/advanced/`` for the music folder and ``tests/data`` for
|
||||
playlists.
|
||||
|
||||
|
||||
Documentation writing
|
||||
=====================
|
||||
|
||||
@ -106,15 +61,26 @@ Creating releases
|
||||
git checkout master
|
||||
git merge --no-ff -m "Release v0.16.0" develop
|
||||
|
||||
#. Install/upgrade tools used for packaging::
|
||||
|
||||
pip install -U twine wheel
|
||||
|
||||
#. Build package and test it manually in a new virtualenv. The following
|
||||
assumes the use of virtualenvwrapper::
|
||||
|
||||
python setup.py sdist
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
mktmpenv
|
||||
pip install path/to/dist/Mopidy-0.16.0.tar.gz
|
||||
toggleglobalsitepackages
|
||||
# do manual test
|
||||
deactivate
|
||||
|
||||
Then test Mopidy manually to confirm that the package is working correctly.
|
||||
mktmpenv
|
||||
pip install path/to/dist/Mopidy-0.16.0-py27-none-any.whl
|
||||
toggleglobalsitepackages
|
||||
# do manual test
|
||||
deactivate
|
||||
|
||||
#. Tag the release::
|
||||
|
||||
@ -125,14 +91,10 @@ Creating releases
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
#. Build source package and upload to PyPI::
|
||||
#. Upload the previously built and tested sdist and bdist_wheel packages to
|
||||
PyPI::
|
||||
|
||||
python setup.py sdist upload
|
||||
|
||||
#. Build wheel package and upload to PyPI::
|
||||
|
||||
pip install -U wheel
|
||||
python setup.py bdist_wheel upload
|
||||
twine upload dist/Mopidy-0.16.0*
|
||||
|
||||
#. Merge ``master`` back into ``develop`` and push the branch to GitHub.
|
||||
|
||||
|
||||
@ -1,37 +1,22 @@
|
||||
.. _ext:
|
||||
|
||||
**********
|
||||
Extensions
|
||||
**********
|
||||
|
||||
Here you can find a list of packages that extend Mopidy with additional
|
||||
functionality. This list is moderated and updated on a regular basis. If you
|
||||
want your package to show up here, follow the :ref:`guide on creating
|
||||
extensions <extensiondev>`.
|
||||
|
||||
|
||||
Bundled with Mopidy
|
||||
===================
|
||||
|
||||
These extensions are maintained by Mopidy's core developers. They are installed
|
||||
together with Mopidy and are enabled by default.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
**
|
||||
|
||||
|
||||
*******************
|
||||
External extensions
|
||||
===================
|
||||
*******************
|
||||
|
||||
These extensions are maintained outside Mopidy's core, often by other
|
||||
developers.
|
||||
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>`.
|
||||
|
||||
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
|
||||
|
||||
@ -40,7 +25,7 @@ and tested with an Arcam AVR-300.
|
||||
|
||||
|
||||
Mopidy-Beets
|
||||
------------
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy-beets
|
||||
|
||||
@ -48,8 +33,17 @@ Provides a backend for playing music from your `Beets
|
||||
<http://beets.radbox.org/>`_ music library through Beets' web extension.
|
||||
|
||||
|
||||
Mopidy-Dirble
|
||||
=============
|
||||
|
||||
https://github.com/mopidy/mopidy-dirble
|
||||
|
||||
Provides a backend for browsing the Internet radio channels from the `Dirble
|
||||
<http://dirble.com/>`_ directory.
|
||||
|
||||
|
||||
Mopidy-GMusic
|
||||
-------------
|
||||
=============
|
||||
|
||||
https://github.com/hechtus/mopidy-gmusic
|
||||
|
||||
@ -58,7 +52,7 @@ Provides a backend for playing music from `Google Play Music
|
||||
|
||||
|
||||
Mopidy-MPRIS
|
||||
------------
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy-mpris
|
||||
|
||||
@ -67,7 +61,7 @@ D-Bus interface, for example using the Ubuntu Sound Menu.
|
||||
|
||||
|
||||
Mopidy-NAD
|
||||
----------
|
||||
==========
|
||||
|
||||
https://github.com/mopidy/mopidy-nad
|
||||
|
||||
@ -75,7 +69,7 @@ Extension for controlling volume using an external NAD amplifier.
|
||||
|
||||
|
||||
Mopidy-Notifier
|
||||
---------------
|
||||
===============
|
||||
|
||||
https://github.com/sauberfred/mopidy-notifier
|
||||
|
||||
@ -83,7 +77,7 @@ Extension for displaying track info as User Notifications in Mac OS X.
|
||||
|
||||
|
||||
Mopidy-radio-de
|
||||
---------------
|
||||
===============
|
||||
|
||||
https://github.com/hechtus/mopidy-radio-de
|
||||
|
||||
@ -93,7 +87,7 @@ Extension for listening to Internet radio stations and podcasts listed at
|
||||
|
||||
|
||||
Mopidy-Scrobbler
|
||||
----------------
|
||||
================
|
||||
|
||||
https://github.com/mopidy/mopidy-scrobbler
|
||||
|
||||
@ -101,7 +95,7 @@ Extension for scrobbling played tracks to Last.fm.
|
||||
|
||||
|
||||
Mopidy-SomaFM
|
||||
-------------
|
||||
=============
|
||||
|
||||
https://github.com/AlexandrePTJ/mopidy-somafm
|
||||
|
||||
@ -110,7 +104,7 @@ service.
|
||||
|
||||
|
||||
Mopidy-SoundCloud
|
||||
-----------------
|
||||
=================
|
||||
|
||||
https://github.com/mopidy/mopidy-soundcloud
|
||||
|
||||
@ -119,7 +113,7 @@ Provides a backend for playing music from the `SoundCloud
|
||||
|
||||
|
||||
Mopidy-Spotify
|
||||
--------------
|
||||
==============
|
||||
|
||||
https://github.com/mopidy/mopidy-spotify
|
||||
|
||||
@ -128,9 +122,18 @@ streaming service.
|
||||
|
||||
|
||||
Mopidy-Subsonic
|
||||
---------------
|
||||
===============
|
||||
|
||||
https://github.com/rattboi/mopidy-subsonic
|
||||
|
||||
Provides a backend for playing music from a `Subsonic Music Streamer
|
||||
<http://www.subsonic.org/>`_ library.
|
||||
|
||||
|
||||
Mopidy-VKontakte
|
||||
================
|
||||
|
||||
https://github.com/sibuser/mopidy-vkontakte
|
||||
|
||||
Provides a backend for playing music from the `VKontakte social network
|
||||
<http://vk.com/>`_.
|
||||
@ -4,33 +4,77 @@
|
||||
Mopidy-HTTP
|
||||
***********
|
||||
|
||||
The HTTP extension lets you control Mopidy through HTTP and WebSockets, e.g.
|
||||
from a web based client. See :ref:`http-api` for details on how to integrate
|
||||
with Mopidy over HTTP.
|
||||
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.
|
||||
|
||||
When it is enabled it starts a web server at the port specified by the
|
||||
:confval:`http/port` config value.
|
||||
|
||||
.. warning::
|
||||
|
||||
As a simple security measure, the web server is by default only available
|
||||
from localhost. To make it available from other computers, change the
|
||||
:confval:`http/hostname` config value. Before you do so, note that the HTTP
|
||||
extension does not feature any form of user authentication or
|
||||
authorization. Anyone able to access the web server can use the full core
|
||||
API of Mopidy. Thus, you probably only want to make the web server
|
||||
available from your local network or place it behind a web proxy which
|
||||
takes care or user authentication. You have been warned.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
Using a web based Mopidy client
|
||||
===============================
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=HTTP+frontend
|
||||
Mopidy-HTTP's web server can also host any static files, for example the HTML,
|
||||
CSS, JavaScript, and images needed for a web based Mopidy client. To host
|
||||
static files, change the :confval:`http/static_dir` config value to point to
|
||||
the root directory of your web client, for example::
|
||||
|
||||
[http]
|
||||
static_dir = /home/alice/dev/the-client
|
||||
|
||||
If the directory includes a file named ``index.html``, it will be served on the
|
||||
root of Mopidy's web server.
|
||||
|
||||
If you're making a web based client and wants to do server side development as
|
||||
well, you are of course free to run your own web server and just use Mopidy's
|
||||
web server to host the API end points. But, for clients implemented purely in
|
||||
JavaScript, letting Mopidy host the files is a simpler solution.
|
||||
|
||||
See :ref:`http-api` for details on how to integrate with Mopidy over HTTP. If
|
||||
you're looking for a web based client for Mopidy, go check out
|
||||
:ref:`http-clients`.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
.. literalinclude:: ../../requirements/http.txt
|
||||
In addition to Mopidy's dependencies, Mopidy-HTTP requires the following:
|
||||
|
||||
- cherrypy >= 3.2.2. Available as python-cherrypy3 in Debian/Ubuntu.
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
Configuration
|
||||
=============
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/http/ext.conf
|
||||
See :ref:`config` for general help on configuring Mopidy.
|
||||
|
||||
.. literalinclude:: ../../mopidy/http/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: http/enabled
|
||||
|
||||
If the HTTP extension should be enabled or not.
|
||||
@ -65,46 +109,3 @@ Configuration values
|
||||
``$hostname`` and ``$port`` can be used in the name.
|
||||
|
||||
Set to an empty string to disable Zeroconf for HTTP.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default if all dependencies are available.
|
||||
|
||||
When it is enabled it starts a web server at the port specified by the
|
||||
:confval:`http/port` config value.
|
||||
|
||||
.. warning:: Security
|
||||
|
||||
As a simple security measure, the web server is by default only available
|
||||
from localhost. To make it available from other computers, change the
|
||||
:confval:`http/hostname` config value. Before you do so, note that the HTTP
|
||||
extension does not feature any form of user authentication or
|
||||
authorization. Anyone able to access the web server can use the full core
|
||||
API of Mopidy. Thus, you probably only want to make the web server
|
||||
available from your local network or place it behind a web proxy which
|
||||
takes care or user authentication. You have been warned.
|
||||
|
||||
|
||||
Using a web based Mopidy client
|
||||
-------------------------------
|
||||
|
||||
The web server can also host any static files, for example the HTML, CSS,
|
||||
JavaScript, and images needed for a web based Mopidy client. To host static
|
||||
files, change the ``http/static_dir`` to point to the root directory of your
|
||||
web client, e.g.::
|
||||
|
||||
[http]
|
||||
static_dir = /home/alice/dev/the-client
|
||||
|
||||
If the directory includes a file named ``index.html``, it will be served on the
|
||||
root of Mopidy's web server.
|
||||
|
||||
If you're making a web based client and wants to do server side development as
|
||||
well, you are of course free to run your own web server and just use Mopidy's
|
||||
web server for the APIs. But, for clients implemented purely in JavaScript,
|
||||
letting Mopidy host the files is a simpler solution.
|
||||
|
||||
If you're looking for a web based client for Mopidy, go check out
|
||||
:ref:`http-clients`.
|
||||
|
||||
@ -4,87 +4,95 @@
|
||||
Mopidy-Local
|
||||
************
|
||||
|
||||
Extension for playing music from a local music archive.
|
||||
Mopidy-Local is an extension for playing music from your local music archive.
|
||||
It is bundled with Mopidy and enabled by default. Though, you'll have to scan
|
||||
your music collection to build a cache of metadata before the Mopidy-Local
|
||||
will be able to play your music.
|
||||
|
||||
This backend handles URIs starting with ``local:``.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
.. _generating-a-local-library:
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Local+backend
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/local/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: local/enabled
|
||||
|
||||
If the local extension should be enabled or not.
|
||||
|
||||
.. confval:: local/media_dir
|
||||
|
||||
Path to directory with local media files.
|
||||
|
||||
.. confval:: local/playlists_dir
|
||||
|
||||
Path to playlists directory with m3u files for local media.
|
||||
|
||||
.. confval:: local/tag_cache_file
|
||||
|
||||
Path to tag cache for local media.
|
||||
|
||||
.. confval:: local/scan_timeout
|
||||
|
||||
Number of milliseconds before giving up scanning a file and moving on to
|
||||
the next file.
|
||||
|
||||
.. confval:: local/excluded_file_extensions
|
||||
|
||||
File extensions to exclude when scanning the media directory.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
If you want use Mopidy to play music you have locally at your machine, you need
|
||||
to review and maybe change some of the local extension config values. See above
|
||||
for a complete list. Then you need to generate a tag cache for your local
|
||||
music...
|
||||
|
||||
|
||||
.. _generating-a-tag-cache:
|
||||
|
||||
Generating a tag cache
|
||||
----------------------
|
||||
Generating a local library
|
||||
==========================
|
||||
|
||||
The command :command:`mopidy local scan` will scan the path set in the
|
||||
:confval:`local/media_dir` config value for any media files and build a MPD
|
||||
compatible ``tag_cache``.
|
||||
:confval:`local/media_dir` config value for any audio files and build a
|
||||
library of metadata.
|
||||
|
||||
To make a ``tag_cache`` of your local music available for Mopidy:
|
||||
To make a local library for your music available for Mopidy:
|
||||
|
||||
#. Ensure that the :confval:`local/media_dir` config value points to where your
|
||||
music is located. Check the current setting by running::
|
||||
|
||||
mopidy config
|
||||
|
||||
#. Scan your media library. The command writes the ``tag_cache`` to
|
||||
the :confval:`local/tag_cache_file`::
|
||||
#. Scan your media library.::
|
||||
|
||||
mopidy local scan
|
||||
|
||||
#. Start Mopidy, find the music library in a client, and play some local music!
|
||||
|
||||
|
||||
Pluggable library support
|
||||
=========================
|
||||
|
||||
Local libraries are fully pluggable. What this means is that users may opt to
|
||||
disable the current default library ``json``, replacing it with a third
|
||||
party one. When running :command:`mopidy local scan` Mopidy will populate
|
||||
whatever the current active library is with data. Only one library may be
|
||||
active at a time.
|
||||
|
||||
To create a new library provider you must create class that implements the
|
||||
:class:`mopidy.local.Library` interface and install it in the extension
|
||||
registry under ``local:library``. Any data that the library needs to store on
|
||||
disc should be stored in :confval:`local/data_dir` using the library name as
|
||||
part of the filename or directory to avoid any conflicts.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
See :ref:`config` for general help on configuring Mopidy.
|
||||
|
||||
.. literalinclude:: ../../mopidy/local/ext.conf
|
||||
:language: ini
|
||||
|
||||
.. confval:: local/enabled
|
||||
|
||||
If the local extension should be enabled or not.
|
||||
|
||||
.. confval:: local/library
|
||||
|
||||
Local library provider to use, change this if you want to use a third party
|
||||
library for local files.
|
||||
|
||||
.. confval:: local/media_dir
|
||||
|
||||
Path to directory with local media files.
|
||||
|
||||
.. confval:: local/data_dir
|
||||
|
||||
Path to directory to store local metadata such as libraries and playlists
|
||||
in.
|
||||
|
||||
.. confval:: local/playlists_dir
|
||||
|
||||
Path to playlists directory with m3u files for local media.
|
||||
|
||||
.. confval:: local/scan_timeout
|
||||
|
||||
Number of milliseconds before giving up scanning a file and moving on to
|
||||
the next file.
|
||||
|
||||
.. confval:: local/scan_flush_threshold
|
||||
|
||||
Number of tracks to wait before telling library it should try and store
|
||||
its progress so far. Some libraries might not respect this setting.
|
||||
Set this to zero to disable flushing.
|
||||
|
||||
.. confval:: local/excluded_file_extensions
|
||||
|
||||
File extensions to exclude when scanning the media directory. Values
|
||||
should be separated by either comma or newline.
|
||||
|
||||
@ -4,22 +4,27 @@
|
||||
Mopidy-MPD
|
||||
**********
|
||||
|
||||
This extension implements an MPD server to make Mopidy available to :ref:`MPD
|
||||
clients <mpd-clients>`.
|
||||
Mopidy-MPD is an extension that provides a full MPD server implementation to
|
||||
make Mopidy available to :ref:`MPD clients <mpd-clients>`. It is bundled with
|
||||
Mopidy and enabled by default.
|
||||
|
||||
.. warning::
|
||||
|
||||
As a simple security measure, the MPD server is by default only available
|
||||
from localhost. To make it available from other computers, change the
|
||||
:confval:`mpd/hostname` config value. Before you do so, note that the MPD
|
||||
server does not support any form of encryption and only a single clear
|
||||
text password (see :confval:`mpd/password`) for weak authentication. Anyone
|
||||
able to access the MPD server can control music playback on your computer.
|
||||
Thus, you probably only want to make the MPD server available from your
|
||||
local network. You have been warned.
|
||||
|
||||
MPD stands for Music Player Daemon, which is also the name of the `original MPD
|
||||
server project <http://mpd.wikia.com/>`_. Mopidy does not depend on the
|
||||
original MPD server, but implements the MPD protocol itself, and is thus
|
||||
compatible with clients for the original MPD server.
|
||||
|
||||
For more details on our MPD server implementation, see
|
||||
:mod:`mopidy.frontends.mpd`.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=MPD+frontend
|
||||
For more details on our MPD server implementation, see :mod:`mopidy.mpd`.
|
||||
|
||||
|
||||
Limitations
|
||||
@ -28,6 +33,7 @@ Limitations
|
||||
This is a non exhaustive list of MPD features that Mopidy doesn't support.
|
||||
Items on this list will probably not be supported in the near future.
|
||||
|
||||
- Only a single password is supported. It gives all-or-nothing access.
|
||||
- Toggling of audio outputs is not supported
|
||||
- Channels for client-to-client communication are not supported
|
||||
- Stickers are not supported
|
||||
@ -41,26 +47,17 @@ near future:
|
||||
|
||||
- Modifying stored playlists is not supported
|
||||
- ``tagtypes`` is not supported
|
||||
- Browsing the file system is not supported
|
||||
- Live update of the music database is not supported
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
Configuration
|
||||
=============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
See :ref:`config` for general help on configuring Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/frontends/mpd/ext.conf
|
||||
.. literalinclude:: ../../mopidy/mpd/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: mpd/enabled
|
||||
|
||||
If the MPD extension should be enabled or not.
|
||||
@ -102,27 +99,3 @@ Configuration values
|
||||
``$hostname`` and ``$port`` can be used in the name.
|
||||
|
||||
Set to an empty string to disable Zeroconf for MPD.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The extension is enabled by default. To connect to the server, use an :ref:`MPD
|
||||
client <mpd-clients>`.
|
||||
|
||||
|
||||
.. _use-mpd-on-a-network:
|
||||
|
||||
Connecting from other machines on the network
|
||||
---------------------------------------------
|
||||
|
||||
As a secure default, Mopidy only accepts connections from ``localhost``. If you
|
||||
want to open it for connections from other machines on your network, see
|
||||
the documentation for the :confval:`mpd/hostname` config value.
|
||||
|
||||
If you open up Mopidy for your local network, you should consider turning on
|
||||
MPD password authentication by setting the :confval:`mpd/password` config value
|
||||
to the password you want to use. If the password is set, Mopidy will require
|
||||
MPD clients to provide the password before they can do anything else. Mopidy
|
||||
only supports a single password, and do not support different permission
|
||||
schemes like the original MPD server.
|
||||
|
||||
@ -4,53 +4,41 @@
|
||||
Mopidy-Stream
|
||||
*************
|
||||
|
||||
Extension for playing streaming music.
|
||||
Mopidy-Stream is an extension for playing streaming music. It is bundled with
|
||||
Mopidy and enabled by default.
|
||||
|
||||
The stream backend will handle streaming of URIs matching the
|
||||
:confval:`stream/protocols` config value, assuming the needed GStreamer plugins
|
||||
are installed.
|
||||
This backend does not provide a library or playlist storage. It simply accepts
|
||||
any URI added to Mopidy's tracklist that matches any of the protocols in the
|
||||
:confval:`stream/protocols` config value. It then tries to retrieve metadata
|
||||
and play back the URI using GStreamer. For example, if you're using an MPD
|
||||
client, you'll just have to find your clients "add URI" interface, and provide
|
||||
it with the URI of a stream.
|
||||
|
||||
In addition to playing streams, the extension also understands how to extract
|
||||
streams from a lot of playlist formats. This is convenient as most Internet
|
||||
radio stations links to playlists instead of directly to the radio streams.
|
||||
|
||||
If you're having trouble playing back a stream, run the ``mopidy deps``
|
||||
command to check if you have all relevant GStreamer plugins installed.
|
||||
|
||||
|
||||
Known issues
|
||||
============
|
||||
Configuration
|
||||
=============
|
||||
|
||||
https://github.com/mopidy/mopidy/issues?labels=Stream+backend
|
||||
See :ref:`config` for general help on configuring Mopidy.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None. The extension just needs Mopidy.
|
||||
|
||||
|
||||
Default configuration
|
||||
=====================
|
||||
|
||||
.. literalinclude:: ../../mopidy/backends/stream/ext.conf
|
||||
.. literalinclude:: ../../mopidy/stream/ext.conf
|
||||
:language: ini
|
||||
|
||||
|
||||
Configuration values
|
||||
====================
|
||||
|
||||
.. confval:: stream/enabled
|
||||
|
||||
If the stream extension should be enabled or not.
|
||||
|
||||
.. confval:: stream/protocols
|
||||
|
||||
Whitelist of URI schemas to allow streaming from.
|
||||
Whitelist of URI schemas to allow streaming from. Values should be
|
||||
separated by either comma or newline.
|
||||
|
||||
.. confval:: stream/timeout
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
This backend does not provide a library or similar. It simply takes any URI
|
||||
added to Mopidy's tracklist that matches any of the protocols in the
|
||||
:confval:`stream/protocols` setting and tries to play back the URI using
|
||||
GStreamer. E.g. if you're using an MPD client, you'll just have to find your
|
||||
clients "add URI" interface, and provide it with the direct URI of the stream.
|
||||
|
||||
Currently the stream backend can only work with URIs pointing direcly at
|
||||
streams, and not intermediate playlists which is often used. See :issue:`303`
|
||||
to track the development of playlist expansion support.
|
||||
Number of milliseconds before giving up looking up stream metadata.
|
||||
|
||||
@ -222,8 +222,10 @@ file::
|
||||
include README.rst
|
||||
include mopidy_soundspot/ext.conf
|
||||
|
||||
For details on the ``MANIFEST.in`` file format, check out the `distuitls docs
|
||||
For details on the ``MANIFEST.in`` file format, check out the `distutils docs
|
||||
<http://docs.python.org/2/distutils/sourcedist.html#manifest-template>`_.
|
||||
`check-manifest <https://pypi.python.org/pypi/check-manifest>`_ is a very
|
||||
useful tool to check your ``MANIFEST.in`` file for completeness.
|
||||
|
||||
|
||||
Example __init__.py
|
||||
@ -237,7 +239,7 @@ The root of your Python package should have an ``__version__`` attribute with a
|
||||
class named ``Extension`` which inherits from Mopidy's extension base class,
|
||||
:class:`mopidy.ext.Extension`. This is the class referred to in the
|
||||
``entry_points`` part of ``setup.py``. Any imports of other files in your
|
||||
extension should be kept inside methods. This ensures that this file can be
|
||||
extension should be kept inside methods. This ensures that this file can be
|
||||
imported without raising :exc:`ImportError` exceptions for missing
|
||||
dependencies, etc.
|
||||
|
||||
@ -259,6 +261,7 @@ This is ``mopidy_soundspot/__init__.py``::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pygst
|
||||
@ -271,6 +274,9 @@ This is ``mopidy_soundspot/__init__.py``::
|
||||
|
||||
__version__ = '0.1'
|
||||
|
||||
# If you need to log, use loggers named after the current Python module
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
@ -288,28 +294,29 @@ This is ``mopidy_soundspot/__init__.py``::
|
||||
schema['password'] = config.Secret()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
try:
|
||||
import pysoundspot
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('pysoundspot library not found', e)
|
||||
|
||||
# You will typically only implement one of the next three methods
|
||||
# in a single extension.
|
||||
|
||||
def get_frontend_classes(self):
|
||||
from .frontend import SoundspotFrontend
|
||||
return [SoundspotFrontend]
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .backend import SoundspotBackend
|
||||
return [SoundspotBackend]
|
||||
|
||||
def get_command(self):
|
||||
from .commands import SoundspotCommand
|
||||
return SoundspotCommand()
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
def validate_environment(self):
|
||||
# Any manual checks of the environment to fail early.
|
||||
# Dependencies described by setup.py are checked by Mopidy, so you
|
||||
# should not check their presence here.
|
||||
pass
|
||||
|
||||
def setup(self, registry):
|
||||
# You will typically only do one of the following things in a
|
||||
# single extension.
|
||||
|
||||
# Register a frontend
|
||||
from .frontend import SoundspotFrontend
|
||||
registry.add('frontend', SoundspotFrontend)
|
||||
|
||||
# Register a backend
|
||||
from .backend import SoundspotBackend
|
||||
registry.add('backend', SoundspotBackend)
|
||||
|
||||
# Register a custom GStreamer element
|
||||
from .mixer import SoundspotMixer
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
@ -341,11 +348,11 @@ passed a reference to the core API when it's created. See the
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy import core
|
||||
|
||||
|
||||
class SoundspotFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
class SoundspotFrontend(pykka.ThreadingActor, core.CoreListener):
|
||||
def __init__(self, config, core):
|
||||
super(SoundspotFrontend, self).__init__()
|
||||
self.core = core
|
||||
|
||||
@ -367,11 +374,11 @@ details.
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy import backend
|
||||
|
||||
|
||||
class SoundspotBackend(pykka.ThreadingActor, base.BaseBackend):
|
||||
def __init__(self, audio):
|
||||
class SoundspotBackend(pykka.ThreadingActor, backend.Backend):
|
||||
def __init__(self, config, audio):
|
||||
super(SoundspotBackend, self).__init__()
|
||||
self.audio = audio
|
||||
|
||||
@ -413,8 +420,8 @@ If you want to extend Mopidy's GStreamer pipeline with new custom GStreamer
|
||||
elements, you'll need to register them in GStreamer before they can be used.
|
||||
|
||||
Basically, you just implement your GStreamer element in Python and then make
|
||||
your :meth:`~mopidy.ext.Extension.register_gstreamer_elements` method register
|
||||
all your custom GStreamer elements.
|
||||
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`.
|
||||
@ -434,7 +441,7 @@ Use of Mopidy APIs
|
||||
|
||||
When writing an extension, you should only use APIs documented at
|
||||
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.utils`, may change at
|
||||
any time, and is not something extensions should rely on being stable.
|
||||
any time, and is not something extensions should use.
|
||||
|
||||
|
||||
Logging in extensions
|
||||
@ -449,6 +456,9 @@ as this will be visible in Mopidy's debug log::
|
||||
|
||||
logger = logging.getLogger('mopidy_soundspot')
|
||||
|
||||
# Or even better, use the Python module name as the logger name:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
When logging at logging level ``info`` or higher (i.e. ``warning``, ``error``,
|
||||
and ``critical``, but not ``debug``) the log message will be displayed to all
|
||||
Mopidy users. Thus, the log messages at those levels should be well written and
|
||||
|
||||
@ -35,12 +35,37 @@ Usage
|
||||
installation/index
|
||||
installation/raspberrypi
|
||||
config
|
||||
ext/index
|
||||
running
|
||||
clients/index
|
||||
troubleshooting
|
||||
|
||||
|
||||
.. _ext:
|
||||
|
||||
Extensions
|
||||
==========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
ext/local
|
||||
ext/stream
|
||||
ext/http
|
||||
ext/mpd
|
||||
ext/external
|
||||
|
||||
|
||||
Clients
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
clients/http
|
||||
clients/mpd
|
||||
clients/mpris
|
||||
clients/upnp
|
||||
|
||||
|
||||
About
|
||||
=====
|
||||
|
||||
|
||||
@ -42,6 +42,19 @@ in the same way as you get updates to the rest of your distribution.
|
||||
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`.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
@ -92,11 +105,11 @@ package found in AUR.
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
OS X: Install from Homebrew and Pip
|
||||
OS X: Install from Homebrew and pip
|
||||
===================================
|
||||
|
||||
If you are running OS X, you can install everything needed with Homebrew and
|
||||
Pip.
|
||||
pip.
|
||||
|
||||
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
|
||||
|
||||
@ -114,7 +127,7 @@ Pip.
|
||||
|
||||
#. Install the required packages from Homebrew::
|
||||
|
||||
brew install gst-python010 gst-plugins-good010 gst-plugins-ugly010 libspotify
|
||||
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
|
||||
@ -129,32 +142,48 @@ Pip.
|
||||
|
||||
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
|
||||
#. 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 get, build, and install the latest release of pyspotify, pylast,
|
||||
and Mopidy using Pip::
|
||||
#. Then, install the latest release of Mopidy using pip::
|
||||
|
||||
sudo pip install -U pyspotify pylast cherrypy ws4py mopidy
|
||||
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
|
||||
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.
|
||||
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
|
||||
#. 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.
|
||||
|
||||
@ -170,49 +199,63 @@ can install Mopidy from PyPI using Pip.
|
||||
|
||||
sudo yum install -y gcc python-devel python-pip
|
||||
|
||||
#. Then you'll need to install all of Mopidy's hard non-Python dependencies:
|
||||
.. note::
|
||||
|
||||
- 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.
|
||||
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
|
||||
following steps.
|
||||
|
||||
If you use Debian/Ubuntu you can install GStreamer like this::
|
||||
#. 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.
|
||||
|
||||
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
If you use Debian/Ubuntu you can install GStreamer like this::
|
||||
|
||||
If you use Arch Linux, install the following packages from the official
|
||||
repository::
|
||||
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
||||
gstreamer0.10-ugly-plugins
|
||||
If you use Arch Linux, install the following packages from the official
|
||||
repository::
|
||||
|
||||
If you use Fedora you can install GStreamer like this::
|
||||
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
||||
gstreamer0.10-ugly-plugins
|
||||
|
||||
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
If you use Fedora you can install GStreamer like this::
|
||||
|
||||
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::
|
||||
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||
|
||||
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \
|
||||
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
|
||||
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::
|
||||
|
||||
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.
|
||||
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 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 Python bindings, pyspotify.
|
||||
libspotify and the Mopidy-Spotify extension.
|
||||
|
||||
#. First, check `pyspotify's changelog <http://pyspotify.mopidy.com/>`_ to
|
||||
see what's the latest version of libspotify which it supports. The
|
||||
versions of libspotify and pyspotify are tightly coupled, so you'll need
|
||||
to get this right.
|
||||
|
||||
#. Download and install the appropriate version of libspotify for your OS
|
||||
and CPU architecture from `Spotify
|
||||
#. 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::
|
||||
@ -221,7 +264,6 @@ can install Mopidy from PyPI using Pip.
|
||||
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
|
||||
sudo ldconfig
|
||||
|
||||
Remember to adjust the above example for the latest libspotify version
|
||||
supported by pyspotify, your OS, and your CPU architecture.
|
||||
@ -232,55 +274,31 @@ can install Mopidy from PyPI using Pip.
|
||||
su -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/libspotify.conf'
|
||||
sudo ldconfig
|
||||
|
||||
#. Then get, build, and install the latest release of pyspotify using Pip::
|
||||
#. Then install the latest release of Mopidy-Spotify using pip::
|
||||
|
||||
sudo pip install -U pyspotify
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U pyspotify
|
||||
sudo pip install -U mopidy-spotify
|
||||
|
||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
||||
pylast::
|
||||
to install Mopidy-Scrobbler::
|
||||
|
||||
sudo pip install -U pylast
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U pylast
|
||||
|
||||
#. Optional: If you want to use the HTTP frontend and web clients, you need
|
||||
cherrypy and ws4py::
|
||||
|
||||
sudo pip install -U cherrypy ws4py
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
|
||||
sudo pip-python install -U cherrypy ws4py
|
||||
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: the Python bindings for libindicate, and the Python bindings
|
||||
for libdbus, the reference D-Bus library.
|
||||
dependencies and the Mopidy-MPRIS extension.
|
||||
|
||||
On Debian/Ubuntu::
|
||||
#. Install the Python bindings for libindicate, and the Python bindings for
|
||||
libdbus, the reference D-Bus library.
|
||||
|
||||
sudo apt-get install python-dbus python-indicate
|
||||
On Debian/Ubuntu::
|
||||
|
||||
#. Then, to install the latest release of Mopidy::
|
||||
sudo apt-get install python-dbus python-indicate
|
||||
|
||||
sudo pip install -U mopidy
|
||||
#. Then install the latest release of Mopidy-MPRIS using pip::
|
||||
|
||||
On Fedora the binary is called ``pip-python``::
|
||||
sudo pip install -U mopidy-mpris
|
||||
|
||||
sudo pip-python 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 mopidy==dev
|
||||
#. 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>`.
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@ -9,7 +9,7 @@ January 2013, Mopidy will run with Spotify support on both the armel
|
||||
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
|
||||
distribution.
|
||||
|
||||
.. image:: /_static/raspberry-pi-by-jwrodgers.jpg
|
||||
.. image:: raspberry-pi-by-jwrodgers.jpg
|
||||
:width: 640
|
||||
:height: 427
|
||||
|
||||
@ -54,14 +54,6 @@ you a lot better performance.
|
||||
|
||||
echo ipv6 | sudo tee -a /etc/modules
|
||||
|
||||
#. Installing Mopidy and its dependencies from `apt.mopidy.com
|
||||
<http://apt.mopidy.com/>`_, as described in :ref:`installation`. In short::
|
||||
|
||||
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mopidy
|
||||
|
||||
#. Since I have a HDMI cable connected, but want the sound on the analog sound
|
||||
connector, I have to run::
|
||||
|
||||
@ -79,9 +71,15 @@ 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`.
|
||||
|
||||
Fixing audio quality issues
|
||||
===========================
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
Appendix: Fixing audio quality issues
|
||||
=====================================
|
||||
|
||||
As of about April 2013 the following steps should resolve any audio
|
||||
issues for HDMI and analog without the use of an external USB sound
|
||||
|
||||
9
docs/modules/local.rst
Normal file
@ -0,0 +1,9 @@
|
||||
************************************
|
||||
:mod:`mopidy.local` -- Local backend
|
||||
************************************
|
||||
|
||||
For details on how to use Mopidy's local backend, see :ref:`ext-local`.
|
||||
|
||||
.. automodule:: mopidy.local
|
||||
:synopsis: Local backend
|
||||
:members:
|
||||
@ -1,17 +1,17 @@
|
||||
*****************************************
|
||||
:mod:`mopidy.frontends.mpd` -- MPD server
|
||||
*****************************************
|
||||
*******************************
|
||||
:mod:`mopidy.mpd` -- MPD server
|
||||
*******************************
|
||||
|
||||
For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd
|
||||
.. automodule:: mopidy.mpd
|
||||
:synopsis: MPD server frontend
|
||||
|
||||
|
||||
MPD dispatcher
|
||||
==============
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.dispatcher
|
||||
.. automodule:: mopidy.mpd.dispatcher
|
||||
:synopsis: MPD request dispatcher
|
||||
:members:
|
||||
|
||||
@ -19,7 +19,7 @@ MPD dispatcher
|
||||
MPD protocol
|
||||
============
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol
|
||||
.. automodule:: mopidy.mpd.protocol
|
||||
:synopsis: MPD protocol
|
||||
:members:
|
||||
|
||||
@ -27,7 +27,7 @@ MPD protocol
|
||||
Audio output
|
||||
------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.audio_output
|
||||
.. automodule:: mopidy.mpd.protocol.audio_output
|
||||
:synopsis: MPD protocol: audio output
|
||||
:members:
|
||||
|
||||
@ -35,7 +35,7 @@ Audio output
|
||||
Channels
|
||||
--------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.channels
|
||||
.. automodule:: mopidy.mpd.protocol.channels
|
||||
:synopsis: MPD protocol: channels -- client to client communication
|
||||
:members:
|
||||
|
||||
@ -43,7 +43,7 @@ Channels
|
||||
Command list
|
||||
------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.command_list
|
||||
.. automodule:: mopidy.mpd.protocol.command_list
|
||||
:synopsis: MPD protocol: command list
|
||||
:members:
|
||||
|
||||
@ -51,7 +51,7 @@ Command list
|
||||
Connection
|
||||
----------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.connection
|
||||
.. automodule:: mopidy.mpd.protocol.connection
|
||||
:synopsis: MPD protocol: connection
|
||||
:members:
|
||||
|
||||
@ -59,7 +59,7 @@ Connection
|
||||
Current playlist
|
||||
----------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.current_playlist
|
||||
.. automodule:: mopidy.mpd.protocol.current_playlist
|
||||
:synopsis: MPD protocol: current playlist
|
||||
:members:
|
||||
|
||||
@ -67,7 +67,7 @@ Current playlist
|
||||
Music database
|
||||
--------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.music_db
|
||||
.. automodule:: mopidy.mpd.protocol.music_db
|
||||
:synopsis: MPD protocol: music database
|
||||
:members:
|
||||
|
||||
@ -75,7 +75,7 @@ Music database
|
||||
Playback
|
||||
--------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.playback
|
||||
.. automodule:: mopidy.mpd.protocol.playback
|
||||
:synopsis: MPD protocol: playback
|
||||
:members:
|
||||
|
||||
@ -83,7 +83,7 @@ Playback
|
||||
Reflection
|
||||
----------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.reflection
|
||||
.. automodule:: mopidy.mpd.protocol.reflection
|
||||
:synopsis: MPD protocol: reflection
|
||||
:members:
|
||||
|
||||
@ -91,7 +91,7 @@ Reflection
|
||||
Status
|
||||
------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.status
|
||||
.. automodule:: mopidy.mpd.protocol.status
|
||||
:synopsis: MPD protocol: status
|
||||
:members:
|
||||
|
||||
@ -99,7 +99,7 @@ Status
|
||||
Stickers
|
||||
--------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.stickers
|
||||
.. automodule:: mopidy.mpd.protocol.stickers
|
||||
:synopsis: MPD protocol: stickers
|
||||
:members:
|
||||
|
||||
@ -107,6 +107,6 @@ Stickers
|
||||
Stored playlists
|
||||
----------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.stored_playlists
|
||||
.. automodule:: mopidy.mpd.protocol.stored_playlists
|
||||
:synopsis: MPD protocol: stored playlists
|
||||
:members:
|
||||
@ -20,8 +20,25 @@ Stopping Mopidy
|
||||
To stop Mopidy, press ``CTRL+C`` in the terminal where you started Mopidy.
|
||||
|
||||
Mopidy will also shut down properly if you send it the TERM signal, e.g. by
|
||||
using ``kill``::
|
||||
using ``pkill``::
|
||||
|
||||
kill `ps ax | grep mopidy | grep -v grep | cut -d' ' -f1`
|
||||
pkill mopidy
|
||||
|
||||
This can be useful e.g. if you create init script for managing Mopidy.
|
||||
|
||||
Init scripts
|
||||
============
|
||||
|
||||
- The ``mopidy`` package at `apt.mopidy.com <http://apt.mopidy.com/>`__ comes
|
||||
with an `sysvinit init script
|
||||
<https://github.com/mopidy/mopidy/blob/debian/debian/mopidy.init>`_.
|
||||
|
||||
- The ``mopidy`` package in `Arch Linux AUR
|
||||
<https://aur.archlinux.org/packages/mopidy>`__ comes with a systemd init
|
||||
script.
|
||||
|
||||
- A blog post by Benjamin Guillet explains how to `Daemonize Mopidy and Launch
|
||||
It at Login on OS X
|
||||
<http://www.benjaminguillet.com/blog/2013/08/16/launch-mopidy-at-login-on-os-x/>`_.
|
||||
|
||||
- Issue :issue:`266` contains a bunch of init scripts for Mopidy, including
|
||||
Upstart init scripts.
|
||||
|
||||
@ -25,8 +25,8 @@ mailing list or when reporting an issue, somewhat longer text dumps are
|
||||
accepted, but large logs should still be shared through a pastebin.
|
||||
|
||||
|
||||
Effective configuration
|
||||
=======================
|
||||
Show effective configuration
|
||||
============================
|
||||
|
||||
The command ``mopidy config`` will print your full effective
|
||||
configuration the way Mopidy sees it after all defaults and all config files
|
||||
@ -35,8 +35,8 @@ passwords are masked out, so the output of the command should be safe to share
|
||||
with others for debugging.
|
||||
|
||||
|
||||
Installed dependencies
|
||||
======================
|
||||
Show installed dependencies
|
||||
===========================
|
||||
|
||||
The command ``mopidy deps`` will list the paths to and versions of
|
||||
any dependency Mopidy or the extensions might need to work. This is very useful
|
||||
@ -48,11 +48,16 @@ your system.
|
||||
Debug logging
|
||||
=============
|
||||
|
||||
If you run :option:`mopidy -v`, Mopidy will output debug log to stdout. If you
|
||||
run :option:`mopidy --save-debug-log`, it will save the debug log to the file
|
||||
``mopidy.log`` in the directory you ran the command from.
|
||||
If you run :option:`mopidy -v` or ``mopidy -vv`` or ``mopidy -vvv`` Mopidy will
|
||||
print more and more debug log to stdout. All three options will give you debug
|
||||
level output from Mopidy and extensions, while ``-vv`` and ``-vvv`` will give
|
||||
you more log output from their dependencies as well.
|
||||
|
||||
If you want to turn on more or less logging for some component, see the
|
||||
If you run :option:`mopidy --save-debug-log`, it will save the log equivalent
|
||||
with ``-vvv`` to the file ``mopidy.log`` in the directory you ran the command
|
||||
from.
|
||||
|
||||
If you want to reduce the logging for some component, see the
|
||||
docs for the :confval:`loglevels/*` config section.
|
||||
|
||||
|
||||
|
||||
@ -11,26 +11,43 @@ module.exports = function (grunt) {
|
||||
" * Licensed under the Apache License, Version 2.0 */\n",
|
||||
files: {
|
||||
own: ["Gruntfile.js", "src/**/*.js", "test/**/*-test.js"],
|
||||
concat: "../mopidy/frontends/http/data/mopidy.js",
|
||||
minified: "../mopidy/frontends/http/data/mopidy.min.js"
|
||||
main: "src/mopidy.js",
|
||||
concat: "../mopidy/http/data/mopidy.js",
|
||||
minified: "../mopidy/http/data/mopidy.min.js"
|
||||
}
|
||||
},
|
||||
buster: {
|
||||
all: {}
|
||||
},
|
||||
concat: {
|
||||
options: {
|
||||
banner: "<%= meta.banner %>",
|
||||
stripBanners: true
|
||||
},
|
||||
all: {
|
||||
browserify: {
|
||||
test_mopidy: {
|
||||
files: {
|
||||
"<%= meta.files.concat %>": [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js",
|
||||
"src/mopidy.js"
|
||||
]
|
||||
"test/lib/mopidy.js": "<%= meta.files.main %>"
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
},
|
||||
test_when: {
|
||||
files: {
|
||||
"test/lib/when.js": "node_modules/when/when.js"
|
||||
},
|
||||
options: {
|
||||
standalone: "when"
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
files: {
|
||||
"<%= meta.files.concat %>": "<%= meta.files.main %>"
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
next(null, grunt.template.process("<%= meta.banner %>") + src);
|
||||
},
|
||||
standalone: "Mopidy"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -70,12 +87,13 @@ module.exports = function (grunt) {
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask("test", ["jshint", "buster"]);
|
||||
grunt.registerTask("build", ["test", "concat", "uglify"]);
|
||||
grunt.registerTask("test_build", ["browserify:test_when", "browserify:test_mopidy"]);
|
||||
grunt.registerTask("test", ["jshint", "test_build", "buster"]);
|
||||
grunt.registerTask("build", ["test", "browserify:dist", "uglify"]);
|
||||
grunt.registerTask("default", ["build"]);
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
grunt.loadNpmTasks("grunt-contrib-concat");
|
||||
grunt.loadNpmTasks("grunt-browserify");
|
||||
grunt.loadNpmTasks("grunt-contrib-jshint");
|
||||
grunt.loadNpmTasks("grunt-contrib-uglify");
|
||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
||||
|
||||
31
js/README.md
@ -21,8 +21,8 @@ You may need to adjust hostname and port for your local setup.
|
||||
|
||||
In the source repo, you can find the files at:
|
||||
|
||||
- `mopidy/frontends/http/data/mopidy.js`
|
||||
- `mopidy/frontends/http/data/mopidy.min.js`
|
||||
- `mopidy/http/data/mopidy.js`
|
||||
- `mopidy/http/data/mopidy.min.js`
|
||||
|
||||
|
||||
Getting it for Node.js use
|
||||
@ -35,7 +35,7 @@ Mopidy.js using npm:
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
var Mopidy = require("mopidy");
|
||||
|
||||
|
||||
Using the library
|
||||
@ -72,7 +72,7 @@ To run tests automatically when you save a file:
|
||||
npm start
|
||||
|
||||
To run tests, concatenate, minify the source, and update the JavaScript files
|
||||
in `mopidy/frontends/http/data/`:
|
||||
in `mopidy/http/data/`:
|
||||
|
||||
npm run-script build
|
||||
|
||||
@ -80,3 +80,26 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in
|
||||
`package.json` and thus isn't available through `npm run-script`:
|
||||
|
||||
PATH=./node_modules/.bin:$PATH grunt foo
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
### 0.2.0 (2014-01-04)
|
||||
|
||||
- **Backwards incompatible change for Node.js users:**
|
||||
`var Mopidy = require('mopidy').Mopidy;` must be changed to
|
||||
`var Mopidy = require('mopidy');`
|
||||
|
||||
- Add support for [Browserify](http://browserify.org/).
|
||||
|
||||
- Upgrade dependencies.
|
||||
|
||||
### 0.1.1 (2013-09-17)
|
||||
|
||||
- Upgrade dependencies.
|
||||
|
||||
### 0.1.0 (2013-03-31)
|
||||
|
||||
- Initial release as a Node.js module to the
|
||||
[npm registry](https://npmjs.org/).
|
||||
|
||||
12
js/buster.js
@ -2,23 +2,13 @@ var config = module.exports;
|
||||
|
||||
config.browser_tests = {
|
||||
environment: "browser",
|
||||
libs: [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js"
|
||||
],
|
||||
sources: ["src/**/*.js"],
|
||||
libs: ["test/lib/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
};
|
||||
|
||||
config.node_tests = {
|
||||
environment: "node",
|
||||
libs: [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js"
|
||||
],
|
||||
sources: ["src/**/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
|
||||
@ -1,171 +0,0 @@
|
||||
/**
|
||||
* BANE - Browser globals, AMD and Node Events
|
||||
*
|
||||
* https://github.com/busterjs/bane
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
((typeof define === "function" && define.amd && function (m) { define("bane", m); }) ||
|
||||
(typeof module === "object" && function (m) { module.exports = m(); }) ||
|
||||
function (m) { this.bane = m(); }
|
||||
)(function () {
|
||||
"use strict";
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
function handleError(event, error, errbacks) {
|
||||
var i, l = errbacks.length;
|
||||
if (l > 0) {
|
||||
for (i = 0; i < l; ++i) { errbacks[i](event, error); }
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
error.message = event + " listener threw error: " + error.message;
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertFunction(fn) {
|
||||
if (typeof fn !== "function") {
|
||||
throw new TypeError("Listener is not function");
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
function supervisors(object) {
|
||||
if (!object.supervisors) { object.supervisors = []; }
|
||||
return object.supervisors;
|
||||
}
|
||||
|
||||
function listeners(object, event) {
|
||||
if (!object.listeners) { object.listeners = {}; }
|
||||
if (event && !object.listeners[event]) { object.listeners[event] = []; }
|
||||
return event ? object.listeners[event] : object.listeners;
|
||||
}
|
||||
|
||||
function errbacks(object) {
|
||||
if (!object.errbacks) { object.errbacks = []; }
|
||||
return object.errbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @signature var emitter = bane.createEmitter([object]);
|
||||
*
|
||||
* Create a new event emitter. If an object is passed, it will be modified
|
||||
* by adding the event emitter methods (see below).
|
||||
*/
|
||||
function createEventEmitter(object) {
|
||||
object = object || {};
|
||||
|
||||
function notifyListener(event, listener, args) {
|
||||
try {
|
||||
listener.listener.apply(listener.thisp || object, args);
|
||||
} catch (e) {
|
||||
handleError(event, e, errbacks(object));
|
||||
}
|
||||
}
|
||||
|
||||
object.on = function (event, listener, thisp) {
|
||||
if (typeof event === "function") {
|
||||
return supervisors(this).push({
|
||||
listener: event,
|
||||
thisp: listener
|
||||
});
|
||||
}
|
||||
listeners(this, event).push({
|
||||
listener: assertFunction(listener),
|
||||
thisp: thisp
|
||||
});
|
||||
};
|
||||
|
||||
object.off = function (event, listener) {
|
||||
var fns, events, i, l;
|
||||
if (!event) {
|
||||
fns = supervisors(this);
|
||||
fns.splice(0, fns.length);
|
||||
|
||||
events = listeners(this);
|
||||
for (i in events) {
|
||||
if (events.hasOwnProperty(i)) {
|
||||
fns = listeners(this, i);
|
||||
fns.splice(0, fns.length);
|
||||
}
|
||||
}
|
||||
|
||||
fns = errbacks(this);
|
||||
fns.splice(0, fns.length);
|
||||
|
||||
return;
|
||||
}
|
||||
if (typeof event === "function") {
|
||||
fns = supervisors(this);
|
||||
listener = event;
|
||||
} else {
|
||||
fns = listeners(this, event);
|
||||
}
|
||||
if (!listener) {
|
||||
fns.splice(0, fns.length);
|
||||
return;
|
||||
}
|
||||
for (i = 0, l = fns.length; i < l; ++i) {
|
||||
if (fns[i].listener === listener) {
|
||||
fns.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
object.once = function (event, listener, thisp) {
|
||||
var wrapper = function () {
|
||||
object.off(event, wrapper);
|
||||
listener.apply(this, arguments);
|
||||
};
|
||||
|
||||
object.on(event, wrapper, thisp);
|
||||
};
|
||||
|
||||
object.bind = function (object, events) {
|
||||
var prop, i, l;
|
||||
if (!events) {
|
||||
for (prop in object) {
|
||||
if (typeof object[prop] === "function") {
|
||||
this.on(prop, object[prop], object);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i = 0, l = events.length; i < l; ++i) {
|
||||
if (typeof object[events[i]] === "function") {
|
||||
this.on(events[i], object[events[i]], object);
|
||||
} else {
|
||||
throw new Error("No such method " + events[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
object.emit = function (event) {
|
||||
var toNotify = supervisors(this);
|
||||
var args = slice.call(arguments), i, l;
|
||||
|
||||
for (i = 0, l = toNotify.length; i < l; ++i) {
|
||||
notifyListener(event, toNotify[i], args);
|
||||
}
|
||||
|
||||
toNotify = listeners(this, event).slice();
|
||||
args = slice.call(arguments, 1);
|
||||
for (i = 0, l = toNotify.length; i < l; ++i) {
|
||||
notifyListener(event, toNotify[i], args);
|
||||
}
|
||||
};
|
||||
|
||||
object.errback = function (listener) {
|
||||
if (!this.errbacks) { this.errbacks = []; }
|
||||
this.errbacks.push(assertFunction(listener));
|
||||
};
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
return { createEventEmitter: createEventEmitter };
|
||||
});
|
||||
1
js/lib/websocket/browser.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = { Client: window.WebSocket };
|
||||
4
js/lib/websocket/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"browser": "browser.js",
|
||||
"main": "server.js"
|
||||
}
|
||||
1
js/lib/websocket/server.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('faye-websocket');
|
||||
@ -1,922 +0,0 @@
|
||||
/** @license MIT License (c) copyright 2011-2013 original author or authors */
|
||||
|
||||
/**
|
||||
* A lightweight CommonJS Promises/A and when() implementation
|
||||
* when is part of the cujo.js family of libraries (http://cujojs.com/)
|
||||
*
|
||||
* Licensed under the MIT License at:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* @author Brian Cavalier
|
||||
* @author John Hann
|
||||
* @version 2.4.0
|
||||
*/
|
||||
(function(define, global) { 'use strict';
|
||||
define(function (require) {
|
||||
|
||||
// Public API
|
||||
|
||||
when.promise = promise; // Create a pending promise
|
||||
when.resolve = resolve; // Create a resolved promise
|
||||
when.reject = reject; // Create a rejected promise
|
||||
when.defer = defer; // Create a {promise, resolver} pair
|
||||
|
||||
when.join = join; // Join 2 or more promises
|
||||
|
||||
when.all = all; // Resolve a list of promises
|
||||
when.map = map; // Array.map() for promises
|
||||
when.reduce = reduce; // Array.reduce() for promises
|
||||
when.settle = settle; // Settle a list of promises
|
||||
|
||||
when.any = any; // One-winner race
|
||||
when.some = some; // Multi-winner race
|
||||
|
||||
when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike
|
||||
when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable
|
||||
|
||||
/**
|
||||
* Register an observer for a promise or immediate value.
|
||||
*
|
||||
* @param {*} promiseOrValue
|
||||
* @param {function?} [onFulfilled] callback to be called when promiseOrValue is
|
||||
* successfully fulfilled. If promiseOrValue is an immediate value, callback
|
||||
* will be invoked immediately.
|
||||
* @param {function?} [onRejected] callback to be called when promiseOrValue is
|
||||
* rejected.
|
||||
* @param {function?} [onProgress] callback to be called when progress updates
|
||||
* are issued for promiseOrValue.
|
||||
* @returns {Promise} a new {@link Promise} that will complete with the return
|
||||
* value of callback or errback or the completion value of promiseOrValue if
|
||||
* callback and/or errback is not supplied.
|
||||
*/
|
||||
function when(promiseOrValue, onFulfilled, onRejected, onProgress) {
|
||||
// Get a trusted promise for the input promiseOrValue, and then
|
||||
// register promise handlers
|
||||
return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted Promise constructor. A Promise created from this constructor is
|
||||
* a trusted when.js promise. Any other duck-typed promise is considered
|
||||
* untrusted.
|
||||
* @constructor
|
||||
* @param {function} sendMessage function to deliver messages to the promise's handler
|
||||
* @param {function?} inspect function that reports the promise's state
|
||||
* @name Promise
|
||||
*/
|
||||
function Promise(sendMessage, inspect) {
|
||||
this._message = sendMessage;
|
||||
this.inspect = inspect;
|
||||
}
|
||||
|
||||
Promise.prototype = {
|
||||
/**
|
||||
* Register handlers for this promise.
|
||||
* @param [onFulfilled] {Function} fulfillment handler
|
||||
* @param [onRejected] {Function} rejection handler
|
||||
* @param [onProgress] {Function} progress handler
|
||||
* @return {Promise} new Promise
|
||||
*/
|
||||
then: function(onFulfilled, onRejected, onProgress) {
|
||||
/*jshint unused:false*/
|
||||
var args, sendMessage;
|
||||
|
||||
args = arguments;
|
||||
sendMessage = this._message;
|
||||
|
||||
return _promise(function(resolve, reject, notify) {
|
||||
sendMessage('when', args, resolve, notify);
|
||||
}, this._status && this._status.observed());
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
|
||||
* @param {function?} onRejected
|
||||
* @return {Promise}
|
||||
*/
|
||||
otherwise: function(onRejected) {
|
||||
return this.then(undef, onRejected);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures that onFulfilledOrRejected will be called regardless of whether
|
||||
* this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT
|
||||
* receive the promises' value or reason. Any returned value will be disregarded.
|
||||
* onFulfilledOrRejected may throw or return a rejected promise to signal
|
||||
* an additional error.
|
||||
* @param {function} onFulfilledOrRejected handler to be called regardless of
|
||||
* fulfillment or rejection
|
||||
* @returns {Promise}
|
||||
*/
|
||||
ensure: function(onFulfilledOrRejected) {
|
||||
return this.then(injectHandler, injectHandler)['yield'](this);
|
||||
|
||||
function injectHandler() {
|
||||
return resolve(onFulfilledOrRejected());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(function() { return value; })
|
||||
* @param {*} value
|
||||
* @return {Promise} a promise that:
|
||||
* - is fulfilled if value is not a promise, or
|
||||
* - if value is a promise, will fulfill with its value, or reject
|
||||
* with its reason.
|
||||
*/
|
||||
'yield': function(value) {
|
||||
return this.then(function() {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs a side effect when this promise fulfills, without changing the
|
||||
* fulfillment value.
|
||||
* @param {function} onFulfilledSideEffect
|
||||
* @returns {Promise}
|
||||
*/
|
||||
tap: function(onFulfilledSideEffect) {
|
||||
return this.then(onFulfilledSideEffect)['yield'](this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Assumes that this promise will fulfill with an array, and arranges
|
||||
* for the onFulfilled to be called with the array as its argument list
|
||||
* i.e. onFulfilled.apply(undefined, array).
|
||||
* @param {function} onFulfilled function to receive spread arguments
|
||||
* @return {Promise}
|
||||
*/
|
||||
spread: function(onFulfilled) {
|
||||
return this.then(function(array) {
|
||||
// array may contain promises, so resolve its contents.
|
||||
return all(array, function(array) {
|
||||
return onFulfilled.apply(undef, array);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected)
|
||||
* @deprecated
|
||||
*/
|
||||
always: function(onFulfilledOrRejected, onProgress) {
|
||||
return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a resolved promise. The returned promise will be
|
||||
* - fulfilled with promiseOrValue if it is a value, or
|
||||
* - if promiseOrValue is a promise
|
||||
* - fulfilled with promiseOrValue's value after it is fulfilled
|
||||
* - rejected with promiseOrValue's reason after it is rejected
|
||||
* @param {*} value
|
||||
* @return {Promise}
|
||||
*/
|
||||
function resolve(value) {
|
||||
return promise(function(resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rejected promise for the supplied promiseOrValue. The returned
|
||||
* promise will be rejected with:
|
||||
* - promiseOrValue, if it is a value, or
|
||||
* - if promiseOrValue is a promise
|
||||
* - promiseOrValue's value after it is fulfilled
|
||||
* - promiseOrValue's reason after it is rejected
|
||||
* @param {*} promiseOrValue the rejected value of the returned {@link Promise}
|
||||
* @return {Promise} rejected {@link Promise}
|
||||
*/
|
||||
function reject(promiseOrValue) {
|
||||
return when(promiseOrValue, rejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {promise, resolver} pair, either or both of which
|
||||
* may be given out safely to consumers.
|
||||
* The resolver has resolve, reject, and progress. The promise
|
||||
* has then plus extended promise API.
|
||||
*
|
||||
* @return {{
|
||||
* promise: Promise,
|
||||
* resolve: function:Promise,
|
||||
* reject: function:Promise,
|
||||
* notify: function:Promise
|
||||
* resolver: {
|
||||
* resolve: function:Promise,
|
||||
* reject: function:Promise,
|
||||
* notify: function:Promise
|
||||
* }}}
|
||||
*/
|
||||
function defer() {
|
||||
var deferred, pending, resolved;
|
||||
|
||||
// Optimize object shape
|
||||
deferred = {
|
||||
promise: undef, resolve: undef, reject: undef, notify: undef,
|
||||
resolver: { resolve: undef, reject: undef, notify: undef }
|
||||
};
|
||||
|
||||
deferred.promise = pending = promise(makeDeferred);
|
||||
|
||||
return deferred;
|
||||
|
||||
function makeDeferred(resolvePending, rejectPending, notifyPending) {
|
||||
deferred.resolve = deferred.resolver.resolve = function(value) {
|
||||
if(resolved) {
|
||||
return resolve(value);
|
||||
}
|
||||
resolved = true;
|
||||
resolvePending(value);
|
||||
return pending;
|
||||
};
|
||||
|
||||
deferred.reject = deferred.resolver.reject = function(reason) {
|
||||
if(resolved) {
|
||||
return resolve(rejected(reason));
|
||||
}
|
||||
resolved = true;
|
||||
rejectPending(reason);
|
||||
return pending;
|
||||
};
|
||||
|
||||
deferred.notify = deferred.resolver.notify = function(update) {
|
||||
notifyPending(update);
|
||||
return update;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise whose fate is determined by resolver.
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
*/
|
||||
function promise(resolver) {
|
||||
return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise, linked to parent, whose fate is determined
|
||||
* by resolver.
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @param {Promise?} status promise from which the new promise is begotten
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
* @private
|
||||
*/
|
||||
function _promise(resolver, status) {
|
||||
var self, value, consumers = [];
|
||||
|
||||
self = new Promise(_message, inspect);
|
||||
self._status = status;
|
||||
|
||||
// Call the provider resolver to seal the promise's fate
|
||||
try {
|
||||
resolver(promiseResolve, promiseReject, promiseNotify);
|
||||
} catch(e) {
|
||||
promiseReject(e);
|
||||
}
|
||||
|
||||
// Return the promise
|
||||
return self;
|
||||
|
||||
/**
|
||||
* Private message delivery. Queues and delivers messages to
|
||||
* the promise's ultimate fulfillment value or rejection reason.
|
||||
* @private
|
||||
* @param {String} type
|
||||
* @param {Array} args
|
||||
* @param {Function} resolve
|
||||
* @param {Function} notify
|
||||
*/
|
||||
function _message(type, args, resolve, notify) {
|
||||
consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); });
|
||||
|
||||
function deliver(p) {
|
||||
p._message(type, args, resolve, notify);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the promise's state at the instant inspect()
|
||||
* is called. The returned object is not live and will not update as
|
||||
* the promise's state changes.
|
||||
* @returns {{ state:String, value?:*, reason?:* }} status snapshot
|
||||
* of the promise.
|
||||
*/
|
||||
function inspect() {
|
||||
return value ? value.inspect() : toPendingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the ultimate fulfillment or rejection
|
||||
* @param {*|Promise} val resolution value
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
if(!consumers) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = coerce(val);
|
||||
scheduleConsumers(consumers, value);
|
||||
consumers = undef;
|
||||
|
||||
if(status) {
|
||||
updateStatus(value, status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject this promise with the supplied reason, which will be used verbatim.
|
||||
* @param {*} reason reason for the rejection
|
||||
*/
|
||||
function promiseReject(reason) {
|
||||
promiseResolve(rejected(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @param {*} update progress event payload to pass to all listeners
|
||||
*/
|
||||
function promiseNotify(update) {
|
||||
if(consumers) {
|
||||
scheduleConsumers(consumers, progressed(update));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fulfilled, local promise as a proxy for a value
|
||||
* NOTE: must never be exposed
|
||||
* @param {*} value fulfillment value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function fulfilled(value) {
|
||||
return near(
|
||||
new NearFulfilledProxy(value),
|
||||
function() { return toFulfilledState(value); }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rejected, local promise with the supplied reason
|
||||
* NOTE: must never be exposed
|
||||
* @param {*} reason rejection reason
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function rejected(reason) {
|
||||
return near(
|
||||
new NearRejectedProxy(reason),
|
||||
function() { return toRejectedState(reason); }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a near promise using the provided proxy
|
||||
* NOTE: must never be exposed
|
||||
* @param {object} proxy proxy for the promise's ultimate value or reason
|
||||
* @param {function} inspect function that returns a snapshot of the
|
||||
* returned near promise's state
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function near(proxy, inspect) {
|
||||
return new Promise(function (type, args, resolve) {
|
||||
try {
|
||||
resolve(proxy[type].apply(proxy, args));
|
||||
} catch(e) {
|
||||
resolve(rejected(e));
|
||||
}
|
||||
}, inspect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress promise with the supplied update.
|
||||
* @private
|
||||
* @param {*} update
|
||||
* @return {Promise} progress promise
|
||||
*/
|
||||
function progressed(update) {
|
||||
return new Promise(function (type, args, _, notify) {
|
||||
var onProgress = args[2];
|
||||
try {
|
||||
notify(typeof onProgress === 'function' ? onProgress(update) : update);
|
||||
} catch(e) {
|
||||
notify(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces x to a trusted Promise
|
||||
*
|
||||
* @private
|
||||
* @param {*} x thing to coerce
|
||||
* @returns {*} Guaranteed to return a trusted Promise. If x
|
||||
* is trusted, returns x, otherwise, returns a new, trusted, already-resolved
|
||||
* Promise whose resolution value is:
|
||||
* * the resolution value of x if it's a foreign promise, or
|
||||
* * x if it's a value
|
||||
*/
|
||||
function coerce(x) {
|
||||
if (x instanceof Promise) {
|
||||
return x;
|
||||
}
|
||||
|
||||
if (!(x === Object(x) && 'then' in x)) {
|
||||
return fulfilled(x);
|
||||
}
|
||||
|
||||
return promise(function(resolve, reject, notify) {
|
||||
enqueue(function() {
|
||||
try {
|
||||
// We must check and assimilate in the same tick, but not the
|
||||
// current tick, careful only to access promiseOrValue.then once.
|
||||
var untrustedThen = x.then;
|
||||
|
||||
if(typeof untrustedThen === 'function') {
|
||||
fcall(untrustedThen, x, resolve, reject, notify);
|
||||
} else {
|
||||
// It's a value, create a fulfilled wrapper
|
||||
resolve(fulfilled(x));
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// Something went wrong, reject
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for a near, fulfilled value
|
||||
* @param {*} value
|
||||
* @constructor
|
||||
*/
|
||||
function NearFulfilledProxy(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
NearFulfilledProxy.prototype.when = function(onResult) {
|
||||
return typeof onResult === 'function' ? onResult(this.value) : this.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy for a near rejection
|
||||
* @param {*} reason
|
||||
* @constructor
|
||||
*/
|
||||
function NearRejectedProxy(reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
NearRejectedProxy.prototype.when = function(_, onError) {
|
||||
if(typeof onError === 'function') {
|
||||
return onError(this.reason);
|
||||
} else {
|
||||
throw this.reason;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule a task that will process a list of handlers
|
||||
* in the next queue drain run.
|
||||
* @private
|
||||
* @param {Array} handlers queue of handlers to execute
|
||||
* @param {*} value passed as the only arg to each handler
|
||||
*/
|
||||
function scheduleConsumers(handlers, value) {
|
||||
enqueue(function() {
|
||||
var handler, i = 0;
|
||||
while (handler = handlers[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(value, status) {
|
||||
value.then(statusFulfilled, statusRejected);
|
||||
|
||||
function statusFulfilled() { status.fulfilled(); }
|
||||
function statusRejected(r) { status.rejected(r); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if x is promise-like, i.e. a thenable object
|
||||
* NOTE: Will return true for *any thenable object*, and isn't truly
|
||||
* safe, since it may attempt to access the `then` property of x (i.e.
|
||||
* clever/malicious getters may do weird things)
|
||||
* @param {*} x anything
|
||||
* @returns {boolean} true if x is promise-like
|
||||
*/
|
||||
function isPromiseLike(x) {
|
||||
return x && typeof x.then === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a competitive race, returning a promise that will resolve when
|
||||
* howMany of the supplied promisesOrValues have resolved, or will reject when
|
||||
* it becomes impossible for howMany to resolve, for example, when
|
||||
* (promisesOrValues.length - howMany) + 1 input promises reject.
|
||||
*
|
||||
* @param {Array} promisesOrValues array of anything, may contain a mix
|
||||
* of promises and values
|
||||
* @param howMany {number} number of promisesOrValues to resolve
|
||||
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
|
||||
* @returns {Promise} promise that will resolve to an array of howMany values that
|
||||
* resolved first, or will reject with an array of
|
||||
* (promisesOrValues.length - howMany) + 1 rejection reasons.
|
||||
*/
|
||||
function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) {
|
||||
|
||||
return when(promisesOrValues, function(promisesOrValues) {
|
||||
|
||||
return promise(resolveSome).then(onFulfilled, onRejected, onProgress);
|
||||
|
||||
function resolveSome(resolve, reject, notify) {
|
||||
var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i;
|
||||
|
||||
len = promisesOrValues.length >>> 0;
|
||||
|
||||
toResolve = Math.max(0, Math.min(howMany, len));
|
||||
values = [];
|
||||
|
||||
toReject = (len - toResolve) + 1;
|
||||
reasons = [];
|
||||
|
||||
// No items in the input, resolve immediately
|
||||
if (!toResolve) {
|
||||
resolve(values);
|
||||
|
||||
} else {
|
||||
rejectOne = function(reason) {
|
||||
reasons.push(reason);
|
||||
if(!--toReject) {
|
||||
fulfillOne = rejectOne = identity;
|
||||
reject(reasons);
|
||||
}
|
||||
};
|
||||
|
||||
fulfillOne = function(val) {
|
||||
// This orders the values based on promise resolution order
|
||||
values.push(val);
|
||||
if (!--toResolve) {
|
||||
fulfillOne = rejectOne = identity;
|
||||
resolve(values);
|
||||
}
|
||||
};
|
||||
|
||||
for(i = 0; i < len; ++i) {
|
||||
if(i in promisesOrValues) {
|
||||
when(promisesOrValues[i], fulfiller, rejecter, notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rejecter(reason) {
|
||||
rejectOne(reason);
|
||||
}
|
||||
|
||||
function fulfiller(val) {
|
||||
fulfillOne(val);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a competitive race, returning a promise that will resolve when
|
||||
* any one of the supplied promisesOrValues has resolved or will reject when
|
||||
* *all* promisesOrValues have rejected.
|
||||
*
|
||||
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
|
||||
* @returns {Promise} promise that will resolve to the value that resolved first, or
|
||||
* will reject with an array of all rejected inputs.
|
||||
*/
|
||||
function any(promisesOrValues, onFulfilled, onRejected, onProgress) {
|
||||
|
||||
function unwrapSingleResult(val) {
|
||||
return onFulfilled ? onFulfilled(val[0]) : val[0];
|
||||
}
|
||||
|
||||
return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that will resolve only once all the supplied promisesOrValues
|
||||
* have resolved. The resolution value of the returned promise will be an array
|
||||
* containing the resolution values of each of the promisesOrValues.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onRejected] DEPRECATED, use returnedPromise.then()
|
||||
* @param {function?} [onProgress] DEPRECATED, use returnedPromise.then()
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function all(promisesOrValues, onFulfilled, onRejected, onProgress) {
|
||||
return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple promises into a single returned promise.
|
||||
* @return {Promise} a promise that will fulfill when *all* the input promises
|
||||
* have fulfilled, or will reject when *any one* of the input promises rejects.
|
||||
*/
|
||||
function join(/* ...promises */) {
|
||||
return _map(arguments, identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settles all input promises such that they are guaranteed not to
|
||||
* be pending once the returned promise fulfills. The returned promise
|
||||
* will always fulfill, except in the case where `array` is a promise
|
||||
* that rejects.
|
||||
* @param {Array|Promise} array or promise for array of promises to settle
|
||||
* @returns {Promise} promise that always fulfills with an array of
|
||||
* outcome snapshots for each input promise.
|
||||
*/
|
||||
function settle(array) {
|
||||
return _map(array, toFulfilledState, toRejectedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise-aware array map function, similar to `Array.prototype.map()`,
|
||||
* but input array may contain promises or values.
|
||||
* @param {Array|Promise} array array of anything, may contain promises and values
|
||||
* @param {function} mapFunc map function which may return a promise or value
|
||||
* @returns {Promise} promise that will fulfill with an array of mapped values
|
||||
* or reject if any input promise rejects.
|
||||
*/
|
||||
function map(array, mapFunc) {
|
||||
return _map(array, mapFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal map that allows a fallback to handle rejections
|
||||
* @param {Array|Promise} array array of anything, may contain promises and values
|
||||
* @param {function} mapFunc map function which may return a promise or value
|
||||
* @param {function?} fallback function to handle rejected promises
|
||||
* @returns {Promise} promise that will fulfill with an array of mapped values
|
||||
* or reject if any input promise rejects.
|
||||
*/
|
||||
function _map(array, mapFunc, fallback) {
|
||||
return when(array, function(array) {
|
||||
|
||||
return _promise(resolveMap);
|
||||
|
||||
function resolveMap(resolve, reject, notify) {
|
||||
var results, len, toResolve, i;
|
||||
|
||||
// Since we know the resulting length, we can preallocate the results
|
||||
// array to avoid array expansions.
|
||||
toResolve = len = array.length >>> 0;
|
||||
results = [];
|
||||
|
||||
if(!toResolve) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Since mapFunc may be async, get all invocations of it into flight
|
||||
for(i = 0; i < len; i++) {
|
||||
if(i in array) {
|
||||
resolveOne(array[i], i);
|
||||
} else {
|
||||
--toResolve;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOne(item, i) {
|
||||
when(item, mapFunc, fallback).then(function(mapped) {
|
||||
results[i] = mapped;
|
||||
notify(mapped);
|
||||
|
||||
if(!--toResolve) {
|
||||
resolve(results);
|
||||
}
|
||||
}, reject);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
|
||||
* input may contain promises and/or values, and reduceFunc
|
||||
* may return either a value or a promise, *and* initialValue may
|
||||
* be a promise for the starting value.
|
||||
*
|
||||
* @param {Array|Promise} promise array or promise for an array of anything,
|
||||
* may contain a mix of promises and values.
|
||||
* @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total),
|
||||
* where total is the total number of items being reduced, and will be the same
|
||||
* in each call to reduceFunc.
|
||||
* @returns {Promise} that will resolve to the final reduced value
|
||||
*/
|
||||
function reduce(promise, reduceFunc /*, initialValue */) {
|
||||
var args = fcall(slice, arguments, 1);
|
||||
|
||||
return when(promise, function(array) {
|
||||
var total;
|
||||
|
||||
total = array.length;
|
||||
|
||||
// Wrap the supplied reduceFunc with one that handles promises and then
|
||||
// delegates to the supplied.
|
||||
args[0] = function (current, val, i) {
|
||||
return when(current, function (c) {
|
||||
return when(val, function (value) {
|
||||
return reduceFunc(c, value, i, total);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return reduceArray.apply(array, args);
|
||||
});
|
||||
}
|
||||
|
||||
// Snapshot states
|
||||
|
||||
/**
|
||||
* Creates a fulfilled state snapshot
|
||||
* @private
|
||||
* @param {*} x any value
|
||||
* @returns {{state:'fulfilled',value:*}}
|
||||
*/
|
||||
function toFulfilledState(x) {
|
||||
return { state: 'fulfilled', value: x };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rejected state snapshot
|
||||
* @private
|
||||
* @param {*} x any reason
|
||||
* @returns {{state:'rejected',reason:*}}
|
||||
*/
|
||||
function toRejectedState(x) {
|
||||
return { state: 'rejected', reason: x };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending state snapshot
|
||||
* @private
|
||||
* @returns {{state:'pending'}}
|
||||
*/
|
||||
function toPendingState() {
|
||||
return { state: 'pending' };
|
||||
}
|
||||
|
||||
//
|
||||
// Internals, utilities, etc.
|
||||
//
|
||||
|
||||
var reduceArray, slice, fcall, nextTick, handlerQueue,
|
||||
setTimeout, funcProto, call, arrayProto, monitorApi,
|
||||
cjsRequire, undef;
|
||||
|
||||
cjsRequire = require;
|
||||
|
||||
//
|
||||
// Shared handler queue processing
|
||||
//
|
||||
// Credit to Twisol (https://github.com/Twisol) for suggesting
|
||||
// this type of extensible queue + trampoline approach for
|
||||
// next-tick conflation.
|
||||
|
||||
handlerQueue = [];
|
||||
|
||||
/**
|
||||
* Enqueue a task. If the queue is not currently scheduled to be
|
||||
* drained, schedule it.
|
||||
* @param {function} task
|
||||
*/
|
||||
function enqueue(task) {
|
||||
if(handlerQueue.push(task) === 1) {
|
||||
nextTick(drainQueue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the handler queue entirely, being careful to allow the
|
||||
* queue to be extended while it is being processed, and to continue
|
||||
* processing until it is truly empty.
|
||||
*/
|
||||
function drainQueue() {
|
||||
var task, i = 0;
|
||||
|
||||
while(task = handlerQueue[i++]) {
|
||||
task();
|
||||
}
|
||||
|
||||
handlerQueue = [];
|
||||
}
|
||||
|
||||
// capture setTimeout to avoid being caught by fake timers
|
||||
// used in time based tests
|
||||
setTimeout = global.setTimeout;
|
||||
|
||||
// Allow attaching the monitor to when() if env has no console
|
||||
monitorApi = typeof console != 'undefined' ? console : when;
|
||||
|
||||
// Prefer setImmediate or MessageChannel, cascade to node,
|
||||
// vertx and finally setTimeout
|
||||
/*global setImmediate,MessageChannel,process*/
|
||||
if (typeof setImmediate === 'function') {
|
||||
nextTick = setImmediate.bind(global);
|
||||
} else if(typeof MessageChannel !== 'undefined') {
|
||||
var channel = new MessageChannel();
|
||||
channel.port1.onmessage = drainQueue;
|
||||
nextTick = function() { channel.port2.postMessage(0); };
|
||||
} else if (typeof process === 'object' && process.nextTick) {
|
||||
nextTick = process.nextTick;
|
||||
} else {
|
||||
try {
|
||||
// vert.x 1.x || 2.x
|
||||
nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext;
|
||||
} catch(ignore) {
|
||||
nextTick = function(t) { setTimeout(t, 0); };
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Capture/polyfill function and array utils
|
||||
//
|
||||
|
||||
// Safe function calls
|
||||
funcProto = Function.prototype;
|
||||
call = funcProto.call;
|
||||
fcall = funcProto.bind
|
||||
? call.bind(call)
|
||||
: function(f, context) {
|
||||
return f.apply(context, slice.call(arguments, 2));
|
||||
};
|
||||
|
||||
// Safe array ops
|
||||
arrayProto = [];
|
||||
slice = arrayProto.slice;
|
||||
|
||||
// ES5 reduce implementation if native not available
|
||||
// See: http://es5.github.com/#x15.4.4.21 as there are many
|
||||
// specifics and edge cases. ES5 dictates that reduce.length === 1
|
||||
// This implementation deviates from ES5 spec in the following ways:
|
||||
// 1. It does not check if reduceFunc is a Callable
|
||||
reduceArray = arrayProto.reduce ||
|
||||
function(reduceFunc /*, initialValue */) {
|
||||
/*jshint maxcomplexity: 7*/
|
||||
var arr, args, reduced, len, i;
|
||||
|
||||
i = 0;
|
||||
arr = Object(this);
|
||||
len = arr.length >>> 0;
|
||||
args = arguments;
|
||||
|
||||
// If no initialValue, use first item of array (we know length !== 0 here)
|
||||
// and adjust i to start at second item
|
||||
if(args.length <= 1) {
|
||||
// Skip to the first real element in the array
|
||||
for(;;) {
|
||||
if(i in arr) {
|
||||
reduced = arr[i++];
|
||||
break;
|
||||
}
|
||||
|
||||
// If we reached the end of the array without finding any real
|
||||
// elements, it's a TypeError
|
||||
if(++i >= len) {
|
||||
throw new TypeError();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If initialValue provided, use it
|
||||
reduced = args[1];
|
||||
}
|
||||
|
||||
// Do the actual reduce
|
||||
for(;i < len; ++i) {
|
||||
if(i in arr) {
|
||||
reduced = reduceFunc(reduced, arr[i], i, arr);
|
||||
}
|
||||
}
|
||||
|
||||
return reduced;
|
||||
};
|
||||
|
||||
function identity(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
return when;
|
||||
});
|
||||
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this);
|
||||
@ -1,11 +0,0 @@
|
||||
if (typeof window !== "undefined") {
|
||||
window.define = function (factory) {
|
||||
try {
|
||||
delete window.define;
|
||||
} catch (e) {
|
||||
window.define = void 0; // IE
|
||||
}
|
||||
window.when = factory();
|
||||
};
|
||||
window.define.amd = {};
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Client lib for controlling a Mopidy music server over a WebSocket",
|
||||
"homepage": "http://www.mopidy.com/",
|
||||
"author": {
|
||||
@ -14,19 +14,19 @@
|
||||
},
|
||||
"main": "src/mopidy.js",
|
||||
"dependencies": {
|
||||
"bane": "~1.0.0",
|
||||
"faye-websocket": "~0.7.0",
|
||||
"when": "~2.4.0"
|
||||
"bane": "~1.1.0",
|
||||
"faye-websocket": "~0.7.2",
|
||||
"when": "~2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.6.13",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-buster": "~0.2.1",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-jshint": "~0.6.4",
|
||||
"grunt-contrib-uglify": "~0.2.4",
|
||||
"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-0"
|
||||
"phantomjs": "~1.9.2-6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
/*global exports:false, require:false*/
|
||||
/*global module:true, require:false*/
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var bane = require("bane");
|
||||
var websocket = require("faye-websocket");
|
||||
var when = require("when");
|
||||
}
|
||||
var bane = require("bane");
|
||||
var websocket = require("../lib/websocket/");
|
||||
var when = require("when");
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
@ -26,11 +24,7 @@ function Mopidy(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
} else {
|
||||
Mopidy.WebSocket = window.WebSocket;
|
||||
}
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
var currentHost = (typeof document !== "undefined" &&
|
||||
@ -295,6 +289,4 @@ Mopidy.prototype._snakeToCamel = function (name) {
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof exports === "object") {
|
||||
exports.Mopidy = Mopidy;
|
||||
}
|
||||
module.exports = Mopidy;
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
/*global require:false, assert:false, refute:false*/
|
||||
/*global require:false */
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var buster = require("buster");
|
||||
var Mopidy = require("../src/mopidy").Mopidy;
|
||||
var Mopidy = require("../src/mopidy");
|
||||
var when = require("when");
|
||||
}
|
||||
|
||||
var assert = buster.assert;
|
||||
var refute = buster.refute;
|
||||
|
||||
buster.testCase("Mopidy", {
|
||||
setUp: function () {
|
||||
// Sinon.JS doesn't manage to stub PhantomJS' WebSocket implementation,
|
||||
|
||||
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.17.0'
|
||||
__version__ = '0.18.0'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
@ -29,7 +29,7 @@ from mopidy import commands, ext
|
||||
from mopidy import config as config_lib
|
||||
from mopidy.utils import log, path, process, versioning
|
||||
|
||||
logger = logging.getLogger('mopidy.main')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
@ -40,11 +40,13 @@ def main():
|
||||
signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
|
||||
|
||||
try:
|
||||
registry = ext.Registry()
|
||||
|
||||
root_cmd = commands.RootCommand()
|
||||
config_cmd = commands.ConfigCommand()
|
||||
deps_cmd = commands.DepsCommand()
|
||||
|
||||
root_cmd.set(extension=None)
|
||||
root_cmd.set(extension=None, registry=registry)
|
||||
root_cmd.add_child('config', config_cmd)
|
||||
root_cmd.add_child('deps', deps_cmd)
|
||||
|
||||
@ -68,7 +70,8 @@ def main():
|
||||
if args.verbosity_level:
|
||||
verbosity_level += args.verbosity_level
|
||||
|
||||
log.setup_logging(config, verbosity_level, args.save_debug_log)
|
||||
log.setup_logging(
|
||||
config, installed_extensions, verbosity_level, args.save_debug_log)
|
||||
|
||||
enabled_extensions = []
|
||||
for extension in installed_extensions:
|
||||
@ -84,7 +87,6 @@ def main():
|
||||
enabled_extensions.append(extension)
|
||||
|
||||
log_extension_info(installed_extensions, enabled_extensions)
|
||||
ext.register_gstreamer_elements(enabled_extensions)
|
||||
|
||||
# Config and deps commands are simply special cased for now.
|
||||
if args.command == config_cmd:
|
||||
@ -108,12 +110,15 @@ def main():
|
||||
args.extension.ext_name)
|
||||
return 1
|
||||
|
||||
for extension in enabled_extensions:
|
||||
extension.setup(registry)
|
||||
|
||||
# Anything that wants to exit after this point must use
|
||||
# mopidy.utils.process.exit_process as actors can have been started.
|
||||
try:
|
||||
return args.command.run(args, proxied_config, enabled_extensions)
|
||||
return args.command.run(args, proxied_config)
|
||||
except NotImplementedError:
|
||||
print root_cmd.format_help()
|
||||
print(root_cmd.format_help())
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@ -129,7 +134,7 @@ def create_file_structures_and_config(args, extensions):
|
||||
|
||||
# Initialize whatever the last config file is with defaults
|
||||
config_file = args.config_files[-1]
|
||||
if os.path.exists(config_file):
|
||||
if os.path.exists(path.expand_path(config_file)):
|
||||
return
|
||||
|
||||
try:
|
||||
@ -161,9 +166,9 @@ def log_extension_info(all_extensions, enabled_extensions):
|
||||
# TODO: distinguish disabled vs blocked by env?
|
||||
enabled_names = set(e.ext_name for e in enabled_extensions)
|
||||
disabled_names = set(e.ext_name for e in all_extensions) - enabled_names
|
||||
logging.info(
|
||||
logger.info(
|
||||
'Enabled extensions: %s', ', '.join(enabled_names) or 'none')
|
||||
logging.info(
|
||||
logger.info(
|
||||
'Disabled extensions: %s', ', '.join(disabled_names) or 'none')
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ from . import mixers, playlists, utils
|
||||
from .constants import PlaybackState
|
||||
from .listener import AudioListener
|
||||
|
||||
logger = logging.getLogger('mopidy.audio')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mixers.register_mixers()
|
||||
|
||||
@ -184,6 +184,7 @@ class Audio(pykka.ThreadingActor):
|
||||
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')
|
||||
@ -192,6 +193,9 @@ class Audio(pykka.ThreadingActor):
|
||||
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:
|
||||
@ -223,11 +227,16 @@ class Audio(pykka.ThreadingActor):
|
||||
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
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
from mopidy import listener
|
||||
|
||||
|
||||
class AudioListener(object):
|
||||
class AudioListener(listener.Listener):
|
||||
"""
|
||||
Marker interface for recipients of events sent by the audio actor.
|
||||
|
||||
@ -17,25 +17,7 @@ class AudioListener(object):
|
||||
@staticmethod
|
||||
def send(event, **kwargs):
|
||||
"""Helper to allow calling of audio listener events"""
|
||||
listeners = pykka.ActorRegistry.get_by_class(AudioListener)
|
||||
for listener in listeners:
|
||||
listener.proxy().on_event(event, **kwargs)
|
||||
|
||||
def on_event(self, event, **kwargs):
|
||||
"""
|
||||
Called on all events.
|
||||
|
||||
*MAY* be implemented by actor. By default, this method forwards the
|
||||
event to the specific event methods.
|
||||
|
||||
For a list of what event names to expect, see the names of the other
|
||||
methods in :class:`AudioListener`.
|
||||
|
||||
:param event: the event name
|
||||
:type event: string
|
||||
:param kwargs: any other arguments to the specific event handlers
|
||||
"""
|
||||
getattr(self, event)(**kwargs)
|
||||
listener.send_async(AudioListener, event, **kwargs)
|
||||
|
||||
def reached_end_of_stream(self):
|
||||
"""
|
||||
|
||||
@ -12,7 +12,7 @@ import gst
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.audio.mixers.auto')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: we might want to add some ranking to the mixers we know about?
|
||||
|
||||
@ -24,24 +24,24 @@ class Scanner(object):
|
||||
"""
|
||||
|
||||
def __init__(self, timeout=1000, min_duration=100):
|
||||
self.timeout_ms = timeout
|
||||
self.min_duration_ms = min_duration
|
||||
self._timeout_ms = timeout
|
||||
self._min_duration_ms = min_duration
|
||||
|
||||
sink = gst.element_factory_make('fakesink')
|
||||
|
||||
audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float')
|
||||
pad_added = lambda src, pad: pad.link(sink.get_pad('sink'))
|
||||
|
||||
self.uribin = gst.element_factory_make('uridecodebin')
|
||||
self.uribin.set_property('caps', audio_caps)
|
||||
self.uribin.connect('pad-added', pad_added)
|
||||
self._uribin = gst.element_factory_make('uridecodebin')
|
||||
self._uribin.set_property('caps', audio_caps)
|
||||
self._uribin.connect('pad-added', pad_added)
|
||||
|
||||
self.pipe = gst.element_factory_make('pipeline')
|
||||
self.pipe.add(self.uribin)
|
||||
self.pipe.add(sink)
|
||||
self._pipe = gst.element_factory_make('pipeline')
|
||||
self._pipe.add(self._uribin)
|
||||
self._pipe.add(sink)
|
||||
|
||||
self.bus = self.pipe.get_bus()
|
||||
self.bus.set_flushing(True)
|
||||
self._bus = self._pipe.get_bus()
|
||||
self._bus.set_flushing(True)
|
||||
|
||||
def scan(self, uri):
|
||||
"""
|
||||
@ -53,60 +53,71 @@ class Scanner(object):
|
||||
"""
|
||||
try:
|
||||
self._setup(uri)
|
||||
data = self._collect()
|
||||
# Make sure uri and duration does not come from tags.
|
||||
data[b'uri'] = uri
|
||||
data[b'mtime'] = self._query_mtime(uri)
|
||||
data[gst.TAG_DURATION] = self._query_duration()
|
||||
tags = self._collect() # Ensure collect before queries.
|
||||
data = {'uri': uri, 'tags': tags,
|
||||
'mtime': self._query_mtime(uri),
|
||||
'duration': self._query_duration()}
|
||||
finally:
|
||||
self._reset()
|
||||
|
||||
if data[gst.TAG_DURATION] < self.min_duration_ms * gst.MSECOND:
|
||||
raise exceptions.ScannerError('Rejecting file with less than %dms '
|
||||
'audio data.' % self.min_duration_ms)
|
||||
return data
|
||||
if self._min_duration_ms is None:
|
||||
return data
|
||||
elif data['duration'] >= self._min_duration_ms * gst.MSECOND:
|
||||
return data
|
||||
|
||||
raise exceptions.ScannerError('Rejecting file with less than %dms '
|
||||
'audio data.' % self._min_duration_ms)
|
||||
|
||||
def _setup(self, uri):
|
||||
"""Primes the pipeline for collection."""
|
||||
self.pipe.set_state(gst.STATE_READY)
|
||||
self.uribin.set_property(b'uri', uri)
|
||||
self.bus.set_flushing(False)
|
||||
self.pipe.set_state(gst.STATE_PAUSED)
|
||||
self._pipe.set_state(gst.STATE_READY)
|
||||
self._uribin.set_property(b'uri', uri)
|
||||
self._bus.set_flushing(False)
|
||||
result = self._pipe.set_state(gst.STATE_PAUSED)
|
||||
if result == gst.STATE_CHANGE_NO_PREROLL:
|
||||
# Live sources don't pre-roll, so set to playing to get data.
|
||||
self._pipe.set_state(gst.STATE_PLAYING)
|
||||
|
||||
def _collect(self):
|
||||
"""Polls for messages to collect data."""
|
||||
start = time.time()
|
||||
timeout_s = self.timeout_ms / float(1000)
|
||||
poll_timeout_ns = 1000
|
||||
data = {}
|
||||
timeout_s = self._timeout_ms / float(1000)
|
||||
tags = {}
|
||||
|
||||
while time.time() - start < timeout_s:
|
||||
message = self.bus.poll(gst.MESSAGE_ANY, poll_timeout_ns)
|
||||
if not self._bus.have_pending():
|
||||
continue
|
||||
message = self._bus.pop()
|
||||
|
||||
if message is None:
|
||||
pass # polling the bus timed out.
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
if message.type == gst.MESSAGE_ERROR:
|
||||
raise exceptions.ScannerError(message.parse_error()[0])
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
return data
|
||||
return tags
|
||||
elif message.type == gst.MESSAGE_ASYNC_DONE:
|
||||
if message.src == self.pipe:
|
||||
return data
|
||||
if message.src == self._pipe:
|
||||
return tags
|
||||
elif message.type == gst.MESSAGE_TAG:
|
||||
# Taglists are not really dicts, hence the lack of .items() and
|
||||
# explicit .keys. We only keep the last tag for each key, as we
|
||||
# assume this is the best, some formats will produce multiple
|
||||
# taglists. Lastly we force everything to lists for conformity.
|
||||
taglist = message.parse_tag()
|
||||
for key in taglist.keys():
|
||||
data[key] = taglist[key]
|
||||
value = taglist[key]
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
tags[key] = value
|
||||
|
||||
raise exceptions.ScannerError('Timeout after %dms' % self.timeout_ms)
|
||||
raise exceptions.ScannerError('Timeout after %dms' % self._timeout_ms)
|
||||
|
||||
def _reset(self):
|
||||
"""Ensures we cleanup child elements and flush the bus."""
|
||||
self.bus.set_flushing(True)
|
||||
self.pipe.set_state(gst.STATE_NULL)
|
||||
self._bus.set_flushing(True)
|
||||
self._pipe.set_state(gst.STATE_NULL)
|
||||
|
||||
def _query_duration(self):
|
||||
try:
|
||||
return self.pipe.query_duration(gst.FORMAT_TIME, None)[0]
|
||||
return self._pipe.query_duration(gst.FORMAT_TIME, None)[0]
|
||||
except gst.QueryError:
|
||||
return None
|
||||
|
||||
@ -116,78 +127,70 @@ class Scanner(object):
|
||||
return os.path.getmtime(path.uri_to_path(uri))
|
||||
|
||||
|
||||
def _artists(tags, artist_name, artist_id=None):
|
||||
# Name missing, don't set artist
|
||||
if not tags.get(artist_name):
|
||||
return None
|
||||
# One artist name and id, provide artist with id.
|
||||
if len(tags[artist_name]) == 1 and artist_id in tags:
|
||||
return [Artist(name=tags[artist_name][0],
|
||||
musicbrainz_id=tags[artist_id][0])]
|
||||
# Multiple artist, provide artists without id.
|
||||
return [Artist(name=name) for name in tags[artist_name]]
|
||||
|
||||
|
||||
def _date(tags):
|
||||
if not tags.get(gst.TAG_DATE):
|
||||
return None
|
||||
try:
|
||||
date = tags[gst.TAG_DATE][0]
|
||||
return datetime.date(date.year, date.month, date.day).isoformat()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def audio_data_to_track(data):
|
||||
"""Convert taglist data + our extras to a track."""
|
||||
albumartist_kwargs = {}
|
||||
tags = data['tags']
|
||||
album_kwargs = {}
|
||||
artist_kwargs = {}
|
||||
composer_kwargs = {}
|
||||
performer_kwargs = {}
|
||||
track_kwargs = {}
|
||||
|
||||
def _retrieve(source_key, target_key, target):
|
||||
if source_key in data:
|
||||
target[target_key] = data[source_key]
|
||||
track_kwargs['composers'] = _artists(tags, gst.TAG_COMPOSER)
|
||||
track_kwargs['performers'] = _artists(tags, gst.TAG_PERFORMER)
|
||||
track_kwargs['artists'] = _artists(
|
||||
tags, gst.TAG_ARTIST, 'musicbrainz-artistid')
|
||||
album_kwargs['artists'] = _artists(
|
||||
tags, gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid')
|
||||
|
||||
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
_retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs)
|
||||
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
|
||||
_retrieve(gst.TAG_COMPOSER, 'name', composer_kwargs)
|
||||
_retrieve(gst.TAG_PERFORMER, 'name', performer_kwargs)
|
||||
_retrieve(gst.TAG_ALBUM_ARTIST, 'name', albumartist_kwargs)
|
||||
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
|
||||
_retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs)
|
||||
_retrieve(gst.TAG_GENRE, 'genre', track_kwargs)
|
||||
_retrieve(gst.TAG_BITRATE, 'bitrate', track_kwargs)
|
||||
track_kwargs['genre'] = '; '.join(tags.get(gst.TAG_GENRE, []))
|
||||
track_kwargs['name'] = '; '.join(tags.get(gst.TAG_TITLE, []))
|
||||
if not track_kwargs['name']:
|
||||
track_kwargs['name'] = '; '.join(tags.get(gst.TAG_ORGANIZATION, []))
|
||||
|
||||
# Following keys don't seem to have TAG_* constant.
|
||||
_retrieve('comment', 'comment', track_kwargs)
|
||||
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
|
||||
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
|
||||
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
|
||||
_retrieve(
|
||||
'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
|
||||
track_kwargs['comment'] = '; '.join(tags.get('comment', []))
|
||||
if not track_kwargs['comment']:
|
||||
track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_LOCATION, []))
|
||||
if not track_kwargs['comment']:
|
||||
track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_COPYRIGHT, []))
|
||||
|
||||
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
|
||||
date = data[gst.TAG_DATE]
|
||||
try:
|
||||
date = datetime.date(date.year, date.month, date.day)
|
||||
except ValueError:
|
||||
pass # Ignore invalid dates
|
||||
else:
|
||||
track_kwargs['date'] = date.isoformat()
|
||||
track_kwargs['track_no'] = tags.get(gst.TAG_TRACK_NUMBER, [None])[0]
|
||||
track_kwargs['disc_no'] = tags.get(gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0]
|
||||
track_kwargs['bitrate'] = tags.get(gst.TAG_BITRATE, [None])[0]
|
||||
track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0]
|
||||
|
||||
if albumartist_kwargs:
|
||||
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
|
||||
album_kwargs['name'] = tags.get(gst.TAG_ALBUM, [None])[0]
|
||||
album_kwargs['num_tracks'] = tags.get(gst.TAG_TRACK_COUNT, [None])[0]
|
||||
album_kwargs['num_discs'] = tags.get(gst.TAG_ALBUM_VOLUME_COUNT, [None])[0]
|
||||
album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0]
|
||||
|
||||
track_kwargs['date'] = _date(tags)
|
||||
track_kwargs['last_modified'] = int(data.get('mtime') or 0)
|
||||
track_kwargs['length'] = (data.get(gst.TAG_DURATION) or 0) // gst.MSECOND
|
||||
|
||||
# Clear out any empty values we found
|
||||
track_kwargs = {k: v for k, v in track_kwargs.items() if v}
|
||||
album_kwargs = {k: v for k, v in album_kwargs.items() if v}
|
||||
|
||||
track_kwargs['uri'] = data['uri']
|
||||
track_kwargs['last_modified'] = int(data['mtime'])
|
||||
track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND
|
||||
track_kwargs['album'] = Album(**album_kwargs)
|
||||
|
||||
if ('name' in artist_kwargs
|
||||
and not isinstance(artist_kwargs['name'], basestring)):
|
||||
track_kwargs['artists'] = [Artist(name=artist)
|
||||
for artist in artist_kwargs['name']]
|
||||
else:
|
||||
track_kwargs['artists'] = [Artist(**artist_kwargs)]
|
||||
|
||||
if ('name' in composer_kwargs
|
||||
and not isinstance(composer_kwargs['name'], basestring)):
|
||||
track_kwargs['composers'] = [Artist(name=artist)
|
||||
for artist in composer_kwargs['name']]
|
||||
else:
|
||||
track_kwargs['composers'] = \
|
||||
[Artist(**composer_kwargs)] if composer_kwargs else ''
|
||||
|
||||
if ('name' in performer_kwargs
|
||||
and not isinstance(performer_kwargs['name'], basestring)):
|
||||
track_kwargs['performers'] = [Artist(name=artist)
|
||||
for artist in performer_kwargs['name']]
|
||||
else:
|
||||
track_kwargs['performers'] = \
|
||||
[Artist(**performer_kwargs)] if performer_kwargs else ''
|
||||
|
||||
return Track(**track_kwargs)
|
||||
|
||||
300
mopidy/backend/__init__.py
Normal file
@ -0,0 +1,300 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
from mopidy import listener
|
||||
|
||||
|
||||
class Backend(object):
|
||||
#: Actor proxy to an instance of :class:`mopidy.audio.Audio`.
|
||||
#:
|
||||
#: Should be passed to the backend constructor as the kwarg ``audio``,
|
||||
#: which will then set this field.
|
||||
audio = None
|
||||
|
||||
#: The library provider. An instance of
|
||||
#: :class:`~mopidy.backend.LibraryProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide a library.
|
||||
library = None
|
||||
|
||||
#: The playback provider. An instance of
|
||||
#: :class:`~mopidy.backend.PlaybackProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide playback.
|
||||
playback = None
|
||||
|
||||
#: The playlists provider. An instance of
|
||||
#: :class:`~mopidy.backend.PlaylistsProvider`, or class:`None` if
|
||||
#: the backend doesn't provide playlists.
|
||||
playlists = None
|
||||
|
||||
#: List of URI schemes this backend can handle.
|
||||
uri_schemes = []
|
||||
|
||||
# Because the providers is marked as pykka_traversible, we can't get() them
|
||||
# from another actor, and need helper methods to check if the providers are
|
||||
# set or None.
|
||||
|
||||
def has_library(self):
|
||||
return self.library is not None
|
||||
|
||||
def has_library_browse(self):
|
||||
return self.has_library() and self.library.root_directory is not None
|
||||
|
||||
def has_playback(self):
|
||||
return self.playback is not None
|
||||
|
||||
def has_playlists(self):
|
||||
return self.playlists is not None
|
||||
|
||||
|
||||
class LibraryProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backend.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
root_directory = None
|
||||
"""
|
||||
:class:`models.Ref.directory` instance with a URI and name set
|
||||
representing the root of this library's browse tree. URIs must
|
||||
use one of the schemes supported by the backend, and name should
|
||||
be set to a human friendly value.
|
||||
|
||||
*MUST be set by any class that implements :meth:`LibraryProvider.browse`.*
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def browse(self, path):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.browse`.
|
||||
|
||||
If you implement this method, make sure to also set
|
||||
:attr:`root_directory_name`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
return []
|
||||
|
||||
# TODO: replace with search(query, exact=True, ...)
|
||||
def find_exact(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.find_exact`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.refresh`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.search`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlaybackProvider(object):
|
||||
"""
|
||||
:param audio: the audio actor
|
||||
:type audio: actor proxy to an instance of :class:`mopidy.audio.Audio`
|
||||
:param backend: the backend
|
||||
:type backend: :class:`mopidy.backend.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, audio, backend):
|
||||
self.audio = audio
|
||||
self.backend = backend
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause playback.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.pause_playback().get()
|
||||
|
||||
def play(self, track):
|
||||
"""
|
||||
Play given track.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.prepare_change()
|
||||
self.change_track(track)
|
||||
return self.audio.start_playback().get()
|
||||
|
||||
def change_track(self, track):
|
||||
"""
|
||||
Swith to provided track.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.set_uri(track.uri).get()
|
||||
return True
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume playback at the same time position playback was paused.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.start_playback().get()
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seek to a given time position.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.set_position(time_position).get()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.stop_playback().get()
|
||||
|
||||
def get_time_position(self):
|
||||
"""
|
||||
Get the current time position in milliseconds.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
return self.audio.get_position().get()
|
||||
|
||||
|
||||
class PlaylistsProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backend.Backend` instance
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""
|
||||
Currently available playlists.
|
||||
|
||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
return copy.copy(self._playlists)
|
||||
|
||||
@playlists.setter # noqa
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.create`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.delete`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.save`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BackendListener(listener.Listener):
|
||||
"""
|
||||
Marker interface for recipients of events sent by the backend actors.
|
||||
|
||||
Any Pykka actor that mixes in this class will receive calls to the methods
|
||||
defined here when the corresponding events happen in the core actor. This
|
||||
interface is used both for looking up what actors to notify of the events,
|
||||
and for providing default implementations for those listeners that are not
|
||||
interested in all events.
|
||||
|
||||
Normally, only the Core actor should mix in this class.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def send(event, **kwargs):
|
||||
"""Helper to allow calling of backend listener events"""
|
||||
listener.send_async(BackendListener, event, **kwargs)
|
||||
|
||||
def playlists_loaded(self):
|
||||
"""
|
||||
Called when playlists are loaded or refreshed.
|
||||
|
||||
*MAY* be implemented by actor.
|
||||
"""
|
||||
pass
|
||||
111
mopidy/backend/dummy.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""A dummy backend for use in tests.
|
||||
|
||||
This backend implements the backend API in the simplest way possible. It is
|
||||
used in tests of the frontends.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.models import Playlist, Ref, SearchResult
|
||||
|
||||
|
||||
def create_dummy_backend_proxy(config=None, audio=None):
|
||||
return DummyBackend.start(config=config, audio=audio).proxy()
|
||||
|
||||
|
||||
class DummyBackend(pykka.ThreadingActor, backend.Backend):
|
||||
def __init__(self, config, audio):
|
||||
super(DummyBackend, self).__init__()
|
||||
|
||||
self.library = DummyLibraryProvider(backend=self)
|
||||
self.playback = DummyPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = DummyPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = ['dummy']
|
||||
|
||||
|
||||
class DummyLibraryProvider(backend.LibraryProvider):
|
||||
root_directory = Ref.directory(uri='dummy:/', name='dummy')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.dummy_library = []
|
||||
self.dummy_browse_result = {}
|
||||
self.dummy_find_exact_result = SearchResult()
|
||||
self.dummy_search_result = SearchResult()
|
||||
|
||||
def browse(self, path):
|
||||
return self.dummy_browse_result.get(path, [])
|
||||
|
||||
def find_exact(self, **query):
|
||||
return self.dummy_find_exact_result
|
||||
|
||||
def lookup(self, uri):
|
||||
return filter(lambda t: uri == t.uri, self.dummy_library)
|
||||
|
||||
def refresh(self, uri=None):
|
||||
pass
|
||||
|
||||
def search(self, **query):
|
||||
return self.dummy_search_result
|
||||
|
||||
|
||||
class DummyPlaybackProvider(backend.PlaybackProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||
self._time_position = 0
|
||||
|
||||
def pause(self):
|
||||
return True
|
||||
|
||||
def play(self, track):
|
||||
"""Pass a track with URI 'dummy:error' to force failure"""
|
||||
self._time_position = 0
|
||||
return track.uri != 'dummy:error'
|
||||
|
||||
def resume(self):
|
||||
return True
|
||||
|
||||
def seek(self, time_position):
|
||||
self._time_position = time_position
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
return True
|
||||
|
||||
def get_time_position(self):
|
||||
return self._time_position
|
||||
|
||||
|
||||
class DummyPlaylistsProvider(backend.PlaylistsProvider):
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name, uri='dummy:%s' % name)
|
||||
self._playlists.append(playlist)
|
||||
return playlist
|
||||
|
||||
def delete(self, uri):
|
||||
playlist = self.lookup(uri)
|
||||
if playlist:
|
||||
self._playlists.remove(playlist)
|
||||
|
||||
def lookup(self, uri):
|
||||
for playlist in self._playlists:
|
||||
if playlist.uri == uri:
|
||||
return playlist
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
||||
def save(self, playlist):
|
||||
old_playlist = self.lookup(playlist.uri)
|
||||
|
||||
if old_playlist is not None:
|
||||
index = self._playlists.index(old_playlist)
|
||||
self._playlists[index] = playlist
|
||||
else:
|
||||
self._playlists.append(playlist)
|
||||
|
||||
return playlist
|
||||
@ -1,281 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
class Backend(object):
|
||||
#: Actor proxy to an instance of :class:`mopidy.audio.Audio`.
|
||||
#:
|
||||
#: Should be passed to the backend constructor as the kwarg ``audio``,
|
||||
#: which will then set this field.
|
||||
audio = None
|
||||
|
||||
#: The library provider. An instance of
|
||||
#: :class:`~mopidy.backends.base.BaseLibraryProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide a library.
|
||||
library = None
|
||||
|
||||
#: The playback provider. An instance of
|
||||
#: :class:`~mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide playback.
|
||||
playback = None
|
||||
|
||||
#: The playlists provider. An instance of
|
||||
#: :class:`~mopidy.backends.base.BasePlaylistsProvider`, or class:`None` if
|
||||
#: the backend doesn't provide playlists.
|
||||
playlists = None
|
||||
|
||||
#: List of URI schemes this backend can handle.
|
||||
uri_schemes = []
|
||||
|
||||
# Because the providers is marked as pykka_traversible, we can't get() them
|
||||
# from another actor, and need helper methods to check if the providers are
|
||||
# set or None.
|
||||
|
||||
def has_library(self):
|
||||
return self.library is not None
|
||||
|
||||
def has_playback(self):
|
||||
return self.playback is not None
|
||||
|
||||
def has_playlists(self):
|
||||
return self.playlists is not None
|
||||
|
||||
|
||||
class BaseLibraryProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
# TODO: replace with search(query, exact=True, ...)
|
||||
def find_exact(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.find_exact`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.refresh`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.search`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BaseLibraryUpdateProvider(object):
|
||||
uri_schemes = []
|
||||
|
||||
def load(self):
|
||||
"""Loads the library and returns all tracks in it.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def add(self, track):
|
||||
"""Adds given track to library.
|
||||
|
||||
Overwrites any existing track with same URI.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, uri):
|
||||
"""Removes given track from library.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def commit(self):
|
||||
"""Persist changes to library.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BasePlaybackProvider(object):
|
||||
"""
|
||||
:param audio: the audio actor
|
||||
:type audio: actor proxy to an instance of :class:`mopidy.audio.Audio`
|
||||
:param backend: the backend
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, audio, backend):
|
||||
self.audio = audio
|
||||
self.backend = backend
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause playback.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.pause_playback().get()
|
||||
|
||||
def play(self, track):
|
||||
"""
|
||||
Play given track.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.prepare_change()
|
||||
self.change_track(track)
|
||||
return self.audio.start_playback().get()
|
||||
|
||||
def change_track(self, track):
|
||||
"""
|
||||
Swith to provided track.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.audio.set_uri(track.uri).get()
|
||||
return True
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume playback at the same time position playback was paused.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.start_playback().get()
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seek to a given time position.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.set_position(time_position).get()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
return self.audio.stop_playback().get()
|
||||
|
||||
def get_time_position(self):
|
||||
"""
|
||||
Get the current time position in milliseconds.
|
||||
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
return self.audio.get_position().get()
|
||||
|
||||
|
||||
class BasePlaylistsProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""
|
||||
Currently available playlists.
|
||||
|
||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
return copy.copy(self._playlists)
|
||||
|
||||
@playlists.setter # noqa
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.create`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.delete`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
See :meth:`mopidy.core.PlaylistsController.save`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
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,115 +1,5 @@
|
||||
"""A dummy backend for use in tests.
|
||||
|
||||
This backend implements the backend API in the simplest way possible. It is
|
||||
used in tests of the frontends.
|
||||
|
||||
The backend handles URIs starting with ``dummy:``.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
None
|
||||
|
||||
**Default config**
|
||||
|
||||
None
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Playlist, SearchResult
|
||||
|
||||
|
||||
def create_dummy_backend_proxy(config=None, audio=None):
|
||||
return DummyBackend.start(config=config, audio=audio).proxy()
|
||||
|
||||
|
||||
class DummyBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, config, audio):
|
||||
super(DummyBackend, self).__init__()
|
||||
|
||||
self.library = DummyLibraryProvider(backend=self)
|
||||
self.playback = DummyPlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = DummyPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = ['dummy']
|
||||
|
||||
|
||||
class DummyLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.dummy_library = []
|
||||
self.dummy_find_exact_result = SearchResult()
|
||||
self.dummy_search_result = SearchResult()
|
||||
|
||||
def find_exact(self, **query):
|
||||
return self.dummy_find_exact_result
|
||||
|
||||
def lookup(self, uri):
|
||||
return filter(lambda t: uri == t.uri, self.dummy_library)
|
||||
|
||||
def refresh(self, uri=None):
|
||||
pass
|
||||
|
||||
def search(self, **query):
|
||||
return self.dummy_search_result
|
||||
|
||||
|
||||
class DummyPlaybackProvider(base.BasePlaybackProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||
self._time_position = 0
|
||||
|
||||
def pause(self):
|
||||
return True
|
||||
|
||||
def play(self, track):
|
||||
"""Pass a track with URI 'dummy:error' to force failure"""
|
||||
self._time_position = 0
|
||||
return track.uri != 'dummy:error'
|
||||
|
||||
def resume(self):
|
||||
return True
|
||||
|
||||
def seek(self, time_position):
|
||||
self._time_position = time_position
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
return True
|
||||
|
||||
def get_time_position(self):
|
||||
return self._time_position
|
||||
|
||||
|
||||
class DummyPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name, uri='dummy:%s' % name)
|
||||
self._playlists.append(playlist)
|
||||
return playlist
|
||||
|
||||
def delete(self, uri):
|
||||
playlist = self.lookup(uri)
|
||||
if playlist:
|
||||
self._playlists.remove(playlist)
|
||||
|
||||
def lookup(self, uri):
|
||||
for playlist in self._playlists:
|
||||
if playlist.uri == uri:
|
||||
return playlist
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
||||
def save(self, playlist):
|
||||
old_playlist = self.lookup(playlist.uri)
|
||||
|
||||
if old_playlist is not None:
|
||||
index = self._playlists.index(old_playlist)
|
||||
self._playlists[index] = playlist
|
||||
else:
|
||||
self._playlists.append(playlist)
|
||||
|
||||
return playlist
|
||||
# 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,45 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
from mopidy.backend import BackendListener
|
||||
|
||||
|
||||
class BackendListener(object):
|
||||
"""
|
||||
Marker interface for recipients of events sent by the backend actors.
|
||||
|
||||
Any Pykka actor that mixes in this class will receive calls to the methods
|
||||
defined here when the corresponding events happen in the core actor. This
|
||||
interface is used both for looking up what actors to notify of the events,
|
||||
and for providing default implementations for those listeners that are not
|
||||
interested in all events.
|
||||
|
||||
Normally, only the Core actor should mix in this class.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def send(event, **kwargs):
|
||||
"""Helper to allow calling of backend listener events"""
|
||||
listeners = pykka.ActorRegistry.get_by_class(BackendListener)
|
||||
for listener in listeners:
|
||||
listener.proxy().on_event(event, **kwargs)
|
||||
|
||||
def on_event(self, event, **kwargs):
|
||||
"""
|
||||
Called on all events.
|
||||
|
||||
*MAY* be implemented by actor. By default, this method forwards the
|
||||
event to the specific event methods.
|
||||
|
||||
:param event: the event name
|
||||
:type event: string
|
||||
:param kwargs: any other arguments to the specific event handlers
|
||||
"""
|
||||
getattr(self, event)(**kwargs)
|
||||
|
||||
def playlists_loaded(self):
|
||||
"""
|
||||
Called when playlists are loaded or refreshed.
|
||||
|
||||
*MAY* be implemented by actor.
|
||||
"""
|
||||
pass
|
||||
# Make classes previously residing here available in the old location for
|
||||
# backwards compatibility with extensions targeting Mopidy < 0.18.
|
||||
__all__ = ['BackendListener']
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = 'Mopidy-Local'
|
||||
ext_name = 'local'
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
schema['media_dir'] = config.Path()
|
||||
schema['playlists_dir'] = config.Path()
|
||||
schema['tag_cache_file'] = config.Path()
|
||||
schema['scan_timeout'] = config.Integer(
|
||||
minimum=1000, maximum=1000*60*60)
|
||||
schema['excluded_file_extensions'] = config.List(optional=True)
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
pass
|
||||
|
||||
def get_backend_classes(self):
|
||||
from .actor import LocalBackend
|
||||
return [LocalBackend]
|
||||
|
||||
def get_library_updaters(self):
|
||||
from .library import LocalLibraryUpdateProvider
|
||||
return [LocalLibraryUpdateProvider]
|
||||
|
||||
def get_command(self):
|
||||
from .commands import LocalCommand
|
||||
return LocalCommand()
|
||||
@ -1,115 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from mopidy import commands, exceptions
|
||||
from mopidy.audio import scan
|
||||
from mopidy.utils import path
|
||||
|
||||
from . import translator
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local.commands')
|
||||
|
||||
|
||||
class LocalCommand(commands.Command):
|
||||
def __init__(self):
|
||||
super(LocalCommand, self).__init__()
|
||||
self.add_child('scan', ScanCommand())
|
||||
|
||||
|
||||
class ScanCommand(commands.Command):
|
||||
help = "Scan local media files and populate the local library."
|
||||
|
||||
def run(self, args, config, extensions):
|
||||
media_dir = config['local']['media_dir']
|
||||
scan_timeout = config['local']['scan_timeout']
|
||||
excluded_file_extensions = set(
|
||||
ext.lower() for ext in config['local']['excluded_file_extensions'])
|
||||
|
||||
updaters = {}
|
||||
for e in extensions:
|
||||
for updater_class in e.get_library_updaters():
|
||||
if updater_class and 'local' in updater_class.uri_schemes:
|
||||
updaters[e.ext_name] = updater_class
|
||||
|
||||
if not updaters:
|
||||
logger.error('No usable library updaters found.')
|
||||
return 1
|
||||
elif len(updaters) > 1:
|
||||
logger.error('More than one library updater found. '
|
||||
'Provided by: %s', ', '.join(updaters.keys()))
|
||||
return 1
|
||||
|
||||
local_updater = updaters.values()[0](config)
|
||||
|
||||
# TODO: cleanup to consistently use local urls, not a random mix of
|
||||
# local and file uris depending on how the data was loaded.
|
||||
uris_library = set()
|
||||
uris_update = set()
|
||||
uris_remove = set()
|
||||
|
||||
tracks = local_updater.load()
|
||||
logger.info('Checking %d tracks from library.', len(tracks))
|
||||
for track in tracks:
|
||||
try:
|
||||
uri = translator.local_to_file_uri(track.uri, media_dir)
|
||||
stat = os.stat(path.uri_to_path(uri))
|
||||
if int(stat.st_mtime) > track.last_modified:
|
||||
uris_update.add(uri)
|
||||
uris_library.add(uri)
|
||||
except OSError:
|
||||
logger.debug('Missing file %s', track.uri)
|
||||
uris_remove.add(track.uri)
|
||||
|
||||
logger.info('Removing %d missing tracks.', len(uris_remove))
|
||||
for uri in uris_remove:
|
||||
local_updater.remove(uri)
|
||||
|
||||
logger.info('Checking %s for unknown tracks.', media_dir)
|
||||
for uri in path.find_uris(media_dir):
|
||||
file_extension = os.path.splitext(path.uri_to_path(uri))[1]
|
||||
if file_extension.lower() in excluded_file_extensions:
|
||||
logger.debug('Skipped %s: File extension excluded.', uri)
|
||||
continue
|
||||
|
||||
if uri not in uris_library:
|
||||
uris_update.add(uri)
|
||||
|
||||
logger.info('Found %d unknown tracks.', len(uris_update))
|
||||
logger.info('Scanning...')
|
||||
|
||||
scanner = scan.Scanner(scan_timeout)
|
||||
progress = Progress(len(uris_update))
|
||||
|
||||
for uri in sorted(uris_update):
|
||||
try:
|
||||
data = scanner.scan(uri)
|
||||
track = scan.audio_data_to_track(data)
|
||||
local_updater.add(track)
|
||||
logger.debug('Added %s', track.uri)
|
||||
except exceptions.ScannerError as error:
|
||||
logger.warning('Failed %s: %s', uri, error)
|
||||
|
||||
progress.increment()
|
||||
|
||||
logger.info('Commiting changes.')
|
||||
local_updater.commit()
|
||||
return 0
|
||||
|
||||
|
||||
# TODO: move to utils?
|
||||
class Progress(object):
|
||||
def __init__(self, total):
|
||||
self.count = 0
|
||||
self.total = total
|
||||
self.start = time.time()
|
||||
|
||||
def increment(self):
|
||||
self.count += 1
|
||||
if self.count % 1000 == 0 or self.count == self.total:
|
||||
duration = time.time() - self.start
|
||||
remainder = duration / self.count * (self.total - self.count)
|
||||
logger.info('Scanned %d of %d files in %ds, ~%ds left.',
|
||||
self.count, self.total, duration, remainder)
|
||||
@ -1,265 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.frontends.mpd import translator as mpd_translator
|
||||
from mopidy.models import Album, SearchResult
|
||||
|
||||
from .translator import local_to_file_uri, parse_mpd_tag_cache
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._uri_mapping = {}
|
||||
self._media_dir = self.backend.config['local']['media_dir']
|
||||
self._tag_cache_file = self.backend.config['local']['tag_cache_file']
|
||||
self.refresh()
|
||||
|
||||
def _convert_to_int(self, string):
|
||||
try:
|
||||
return int(string)
|
||||
except ValueError:
|
||||
return object()
|
||||
|
||||
def refresh(self, uri=None):
|
||||
logger.debug(
|
||||
'Loading local tracks from %s using %s',
|
||||
self._media_dir, self._tag_cache_file)
|
||||
|
||||
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
|
||||
uris_to_remove = set(self._uri_mapping)
|
||||
|
||||
for track in tracks:
|
||||
self._uri_mapping[track.uri] = track
|
||||
uris_to_remove.discard(track.uri)
|
||||
|
||||
for uri in uris_to_remove:
|
||||
del self._uri_mapping[uri]
|
||||
|
||||
logger.info(
|
||||
'Loaded %d local tracks from %s using %s',
|
||||
len(tracks), self._media_dir, self._tag_cache_file)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
return [self._uri_mapping[uri]]
|
||||
except KeyError:
|
||||
logger.debug('Failed to lookup %r', uri)
|
||||
return []
|
||||
|
||||
def find_exact(self, query=None, uris=None):
|
||||
# TODO Only return results within URI roots given by ``uris``
|
||||
|
||||
if query is None:
|
||||
query = {}
|
||||
self._validate_query(query)
|
||||
result_tracks = self._uri_mapping.values()
|
||||
|
||||
for (field, values) in query.iteritems():
|
||||
if not hasattr(values, '__iter__'):
|
||||
values = [values]
|
||||
# FIXME this is bound to be slow for large libraries
|
||||
for value in values:
|
||||
if field == 'track_no':
|
||||
q = self._convert_to_int(value)
|
||||
else:
|
||||
q = value.strip()
|
||||
|
||||
uri_filter = lambda t: q == t.uri
|
||||
track_name_filter = lambda t: q == t.name
|
||||
album_filter = lambda t: q == getattr(t, 'album', Album()).name
|
||||
artist_filter = lambda t: filter(
|
||||
lambda a: q == a.name, t.artists)
|
||||
albumartist_filter = lambda t: any([
|
||||
q == a.name
|
||||
for a in getattr(t.album, 'artists', [])])
|
||||
composer_filter = lambda t: any([
|
||||
q == a.name
|
||||
for a in getattr(t, 'composers', [])])
|
||||
performer_filter = lambda t: any([
|
||||
q == a.name
|
||||
for a in getattr(t, 'performers', [])])
|
||||
track_no_filter = lambda t: q == t.track_no
|
||||
genre_filter = lambda t: t.genre and q == t.genre
|
||||
date_filter = lambda t: q == t.date
|
||||
comment_filter = lambda t: q == t.comment
|
||||
any_filter = lambda t: (
|
||||
uri_filter(t) or
|
||||
track_name_filter(t) or
|
||||
album_filter(t) or
|
||||
artist_filter(t) or
|
||||
albumartist_filter(t) or
|
||||
composer_filter(t) or
|
||||
performer_filter(t) or
|
||||
track_no_filter(t) or
|
||||
genre_filter(t) or
|
||||
date_filter(t) or
|
||||
comment_filter(t))
|
||||
|
||||
if field == 'uri':
|
||||
result_tracks = filter(uri_filter, result_tracks)
|
||||
elif field == 'track_name':
|
||||
result_tracks = filter(track_name_filter, result_tracks)
|
||||
elif field == 'album':
|
||||
result_tracks = filter(album_filter, result_tracks)
|
||||
elif field == 'artist':
|
||||
result_tracks = filter(artist_filter, result_tracks)
|
||||
elif field == 'albumartist':
|
||||
result_tracks = filter(albumartist_filter, result_tracks)
|
||||
elif field == 'composer':
|
||||
result_tracks = filter(composer_filter, result_tracks)
|
||||
elif field == 'performer':
|
||||
result_tracks = filter(performer_filter, result_tracks)
|
||||
elif field == 'track_no':
|
||||
result_tracks = filter(track_no_filter, result_tracks)
|
||||
elif field == 'genre':
|
||||
result_tracks = filter(genre_filter, result_tracks)
|
||||
elif field == 'date':
|
||||
result_tracks = filter(date_filter, result_tracks)
|
||||
elif field == 'comment':
|
||||
result_tracks = filter(comment_filter, result_tracks)
|
||||
elif field == 'any':
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
# TODO: add local:search:<query>
|
||||
return SearchResult(uri='local:search', tracks=result_tracks)
|
||||
|
||||
def search(self, query=None, uris=None):
|
||||
# TODO Only return results within URI roots given by ``uris``
|
||||
|
||||
if query is None:
|
||||
query = {}
|
||||
self._validate_query(query)
|
||||
result_tracks = self._uri_mapping.values()
|
||||
|
||||
for (field, values) in query.iteritems():
|
||||
if not hasattr(values, '__iter__'):
|
||||
values = [values]
|
||||
# FIXME this is bound to be slow for large libraries
|
||||
for value in values:
|
||||
if field == 'track_no':
|
||||
q = self._convert_to_int(value)
|
||||
else:
|
||||
q = value.strip().lower()
|
||||
|
||||
uri_filter = lambda t: q in t.uri.lower()
|
||||
track_name_filter = lambda t: q in t.name.lower()
|
||||
album_filter = lambda t: q in getattr(
|
||||
t, 'album', Album()).name.lower()
|
||||
artist_filter = lambda t: filter(
|
||||
lambda a: q in a.name.lower(), t.artists)
|
||||
albumartist_filter = lambda t: any([
|
||||
q in a.name.lower()
|
||||
for a in getattr(t.album, 'artists', [])])
|
||||
composer_filter = lambda t: any([
|
||||
q in a.name.lower()
|
||||
for a in getattr(t, 'composers', [])])
|
||||
performer_filter = lambda t: any([
|
||||
q in a.name.lower()
|
||||
for a in getattr(t, 'performers', [])])
|
||||
track_no_filter = lambda t: q == t.track_no
|
||||
genre_filter = lambda t: t.genre and q in t.genre.lower()
|
||||
date_filter = lambda t: t.date and t.date.startswith(q)
|
||||
comment_filter = lambda t: t.comment and q in t.comment.lower()
|
||||
any_filter = lambda t: (
|
||||
uri_filter(t) or
|
||||
track_name_filter(t) or
|
||||
album_filter(t) or
|
||||
artist_filter(t) or
|
||||
albumartist_filter(t) or
|
||||
composer_filter(t) or
|
||||
performer_filter(t) or
|
||||
track_no_filter(t) or
|
||||
genre_filter(t) or
|
||||
date_filter(t) or
|
||||
comment_filter(t))
|
||||
|
||||
if field == 'uri':
|
||||
result_tracks = filter(uri_filter, result_tracks)
|
||||
elif field == 'track_name':
|
||||
result_tracks = filter(track_name_filter, result_tracks)
|
||||
elif field == 'album':
|
||||
result_tracks = filter(album_filter, result_tracks)
|
||||
elif field == 'artist':
|
||||
result_tracks = filter(artist_filter, result_tracks)
|
||||
elif field == 'albumartist':
|
||||
result_tracks = filter(albumartist_filter, result_tracks)
|
||||
elif field == 'composer':
|
||||
result_tracks = filter(composer_filter, result_tracks)
|
||||
elif field == 'performer':
|
||||
result_tracks = filter(performer_filter, result_tracks)
|
||||
elif field == 'track_no':
|
||||
result_tracks = filter(track_no_filter, result_tracks)
|
||||
elif field == 'genre':
|
||||
result_tracks = filter(genre_filter, result_tracks)
|
||||
elif field == 'date':
|
||||
result_tracks = filter(date_filter, result_tracks)
|
||||
elif field == 'comment':
|
||||
result_tracks = filter(comment_filter, result_tracks)
|
||||
elif field == 'any':
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
# TODO: add local:search:<query>
|
||||
return SearchResult(uri='local:search', tracks=result_tracks)
|
||||
|
||||
def _validate_query(self, query):
|
||||
for (_, values) in query.iteritems():
|
||||
if not values:
|
||||
raise LookupError('Missing query')
|
||||
for value in values:
|
||||
if not value:
|
||||
raise LookupError('Missing query')
|
||||
|
||||
|
||||
# TODO: rename and move to tagcache extension.
|
||||
class LocalLibraryUpdateProvider(base.BaseLibraryProvider):
|
||||
uri_schemes = ['local']
|
||||
|
||||
def __init__(self, config):
|
||||
self._tracks = {}
|
||||
self._media_dir = config['local']['media_dir']
|
||||
self._tag_cache_file = config['local']['tag_cache_file']
|
||||
|
||||
def load(self):
|
||||
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
|
||||
for track in tracks:
|
||||
# TODO: this should use uris as is, i.e. hack that should go away
|
||||
# with tag caches.
|
||||
uri = local_to_file_uri(track.uri, self._media_dir)
|
||||
self._tracks[uri] = track.copy(uri=uri)
|
||||
return tracks
|
||||
|
||||
def add(self, track):
|
||||
self._tracks[track.uri] = track
|
||||
|
||||
def remove(self, uri):
|
||||
if uri in self._tracks:
|
||||
del self._tracks[uri]
|
||||
|
||||
def commit(self):
|
||||
directory, basename = os.path.split(self._tag_cache_file)
|
||||
|
||||
# TODO: cleanup directory/basename.* files.
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix=basename + '.', dir=directory, delete=False)
|
||||
|
||||
try:
|
||||
for row in mpd_translator.tracks_to_tag_cache_format(
|
||||
self._tracks.values(), self._media_dir):
|
||||
if len(row) == 1:
|
||||
tmp.write(('%s\n' % row).encode('utf-8'))
|
||||
else:
|
||||
tmp.write(('%s: %s\n' % row).encode('utf-8'))
|
||||
|
||||
os.rename(tmp.name, self._tag_cache_file)
|
||||
finally:
|
||||
if os.path.exists(tmp.name):
|
||||
os.remove(tmp.name)
|
||||
@ -1,17 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy.backends import base
|
||||
|
||||
from . import translator
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalPlaybackProvider(base.BasePlaybackProvider):
|
||||
def change_track(self, track):
|
||||
media_dir = self.backend.config['local']['media_dir']
|
||||
uri = translator.local_to_file_uri(track.uri, media_dir)
|
||||
track = track.copy(uri=uri)
|
||||
return super(LocalPlaybackProvider, self).change_track(track)
|
||||
@ -1,189 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
from mopidy.models import Track, Artist, Album
|
||||
from mopidy.utils.encoding import locale_decode
|
||||
from mopidy.utils.path import path_to_uri, uri_to_path
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
def local_to_file_uri(uri, media_dir):
|
||||
# TODO: check that type is correct.
|
||||
file_path = uri_to_path(uri).split(b':', 1)[1]
|
||||
file_path = os.path.join(media_dir, file_path)
|
||||
return path_to_uri(file_path)
|
||||
|
||||
|
||||
def parse_m3u(file_path, media_dir):
|
||||
r"""
|
||||
Convert M3U file list of uris
|
||||
|
||||
Example M3U data::
|
||||
|
||||
# This is a comment
|
||||
Alternative\Band - Song.mp3
|
||||
Classical\Other Band - New Song.mp3
|
||||
Stuff.mp3
|
||||
D:\More Music\Foo.mp3
|
||||
http://www.example.com:8000/Listen.pls
|
||||
http://www.example.com/~user/Mine.mp3
|
||||
|
||||
- Relative paths of songs should be with respect to location of M3U.
|
||||
- Paths are normaly platform specific.
|
||||
- Lines starting with # should be ignored.
|
||||
- m3u files are latin-1.
|
||||
- This function does not bother with Extended M3U directives.
|
||||
"""
|
||||
# TODO: uris as bytes
|
||||
uris = []
|
||||
try:
|
||||
with open(file_path) as m3u:
|
||||
contents = m3u.readlines()
|
||||
except IOError as error:
|
||||
logger.warning('Couldn\'t open m3u: %s', locale_decode(error))
|
||||
return uris
|
||||
|
||||
for line in contents:
|
||||
line = line.strip().decode('latin1')
|
||||
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
|
||||
if urlparse.urlsplit(line).scheme:
|
||||
uris.append(line)
|
||||
elif os.path.normpath(line) == os.path.abspath(line):
|
||||
path = path_to_uri(line)
|
||||
uris.append(path)
|
||||
else:
|
||||
path = path_to_uri(os.path.join(media_dir, line))
|
||||
uris.append(path)
|
||||
|
||||
return uris
|
||||
|
||||
|
||||
# TODO: remove music_dir from API
|
||||
def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
||||
"""
|
||||
Converts a MPD tag_cache into a lists of tracks, artists and albums.
|
||||
"""
|
||||
tracks = set()
|
||||
|
||||
try:
|
||||
with open(tag_cache) as library:
|
||||
contents = library.read()
|
||||
except IOError as error:
|
||||
logger.warning('Could not open tag cache: %s', locale_decode(error))
|
||||
return tracks
|
||||
|
||||
current = {}
|
||||
state = None
|
||||
|
||||
# TODO: uris as bytes
|
||||
for line in contents.split(b'\n'):
|
||||
if line == b'songList begin':
|
||||
state = 'songs'
|
||||
continue
|
||||
elif line == b'songList end':
|
||||
state = None
|
||||
continue
|
||||
elif not state:
|
||||
continue
|
||||
|
||||
key, value = line.split(b': ', 1)
|
||||
|
||||
if key == b'key':
|
||||
_convert_mpd_data(current, tracks)
|
||||
current.clear()
|
||||
|
||||
current[key.lower()] = value.decode('utf-8')
|
||||
|
||||
_convert_mpd_data(current, tracks)
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def _convert_mpd_data(data, tracks):
|
||||
if not data:
|
||||
return
|
||||
|
||||
track_kwargs = {}
|
||||
album_kwargs = {}
|
||||
artist_kwargs = {}
|
||||
albumartist_kwargs = {}
|
||||
|
||||
if 'track' in data:
|
||||
if '/' in data['track']:
|
||||
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
|
||||
track_kwargs['track_no'] = int(data['track'].split('/')[0])
|
||||
else:
|
||||
track_kwargs['track_no'] = int(data['track'])
|
||||
|
||||
if 'mtime' in data:
|
||||
track_kwargs['last_modified'] = int(data['mtime'])
|
||||
|
||||
if 'artist' in data:
|
||||
artist_kwargs['name'] = data['artist']
|
||||
|
||||
if 'albumartist' in data:
|
||||
albumartist_kwargs['name'] = data['albumartist']
|
||||
|
||||
if 'composer' in data:
|
||||
track_kwargs['composers'] = [Artist(name=data['composer'])]
|
||||
|
||||
if 'performer' in data:
|
||||
track_kwargs['performers'] = [Artist(name=data['performer'])]
|
||||
|
||||
if 'album' in data:
|
||||
album_kwargs['name'] = data['album']
|
||||
|
||||
if 'title' in data:
|
||||
track_kwargs['name'] = data['title']
|
||||
|
||||
if 'genre' in data:
|
||||
track_kwargs['genre'] = data['genre']
|
||||
|
||||
if 'date' in data:
|
||||
track_kwargs['date'] = data['date']
|
||||
|
||||
if 'comment' in data:
|
||||
track_kwargs['comment'] = data['comment']
|
||||
|
||||
if 'musicbrainz_trackid' in data:
|
||||
track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid']
|
||||
|
||||
if 'musicbrainz_albumid' in data:
|
||||
album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid']
|
||||
|
||||
if 'musicbrainz_artistid' in data:
|
||||
artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid']
|
||||
|
||||
if 'musicbrainz_albumartistid' in data:
|
||||
albumartist_kwargs['musicbrainz_id'] = (
|
||||
data['musicbrainz_albumartistid'])
|
||||
|
||||
if artist_kwargs:
|
||||
artist = Artist(**artist_kwargs)
|
||||
track_kwargs['artists'] = [artist]
|
||||
|
||||
if albumartist_kwargs:
|
||||
albumartist = Artist(**albumartist_kwargs)
|
||||
album_kwargs['artists'] = [albumartist]
|
||||
|
||||
if album_kwargs:
|
||||
album = Album(**album_kwargs)
|
||||
track_kwargs['album'] = album
|
||||
|
||||
if data['file'][0] == '/':
|
||||
path = data['file'][1:]
|
||||
else:
|
||||
path = data['file']
|
||||
|
||||
track_kwargs['uri'] = 'local:track:%s' % path
|
||||
track_kwargs['length'] = int(data.get('time', 0)) * 1000
|
||||
|
||||
track = Track(**track_kwargs)
|
||||
tracks.add(track)
|
||||
@ -1,37 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import audio as audio_lib
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Track
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.stream')
|
||||
|
||||
|
||||
class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||
def __init__(self, config, audio):
|
||||
super(StreamBackend, self).__init__()
|
||||
|
||||
self.library = StreamLibraryProvider(backend=self)
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = None
|
||||
|
||||
self.uri_schemes = audio_lib.supported_uri_schemes(
|
||||
config['stream']['protocols'])
|
||||
|
||||
|
||||
# TODO: Should we consider letting lookup know how to expand common playlist
|
||||
# formats (m3u, pls, etc) for http(s) URIs?
|
||||
class StreamLibraryProvider(base.BaseLibraryProvider):
|
||||
def lookup(self, uri):
|
||||
if urlparse.urlsplit(uri).scheme not in self.backend.uri_schemes:
|
||||
return []
|
||||
# TODO: actually lookup the stream metadata by getting tags in same
|
||||
# way as we do for updating the local library with mopidy.scanner
|
||||
# Note that we would only want the stream metadata at this stage,
|
||||
# not the currently playing track's.
|
||||
return [Track(uri=uri, name=uri)]
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
@ -6,6 +6,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import glib
|
||||
import gobject
|
||||
|
||||
from mopidy import config as config_lib
|
||||
@ -13,7 +14,12 @@ from mopidy.audio import Audio
|
||||
from mopidy.core import Core
|
||||
from mopidy.utils import deps, process, versioning
|
||||
|
||||
logger = logging.getLogger('mopidy.commands')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_default_config = []
|
||||
for base in glib.get_system_config_dirs() + (glib.get_user_config_dir(),):
|
||||
_default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf'))
|
||||
DEFAULT_CONFIG = b':'.join(_default_config)
|
||||
|
||||
|
||||
def config_files_type(value):
|
||||
@ -106,7 +112,7 @@ class Command(object):
|
||||
|
||||
def exit(self, status_code=0, message=None, usage=None):
|
||||
"""Optionally print a message and exit."""
|
||||
print '\n\n'.join(m for m in (usage, message) if m)
|
||||
print('\n\n'.join(m for m in (usage, message) if m))
|
||||
sys.exit(status_code)
|
||||
|
||||
def format_usage(self, prog=None):
|
||||
@ -235,7 +241,7 @@ class RootCommand(Command):
|
||||
self.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='count', dest='verbosity_level', default=0,
|
||||
help='more output (debug level)')
|
||||
help='more output (repeat up to 3 times for even more)')
|
||||
self.add_argument(
|
||||
'--save-debug-log',
|
||||
action='store_true', dest='save_debug_log',
|
||||
@ -243,7 +249,7 @@ class RootCommand(Command):
|
||||
self.add_argument(
|
||||
'--config',
|
||||
action='store', dest='config_files', type=config_files_type,
|
||||
default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf', metavar='FILES',
|
||||
default=DEFAULT_CONFIG, metavar='FILES',
|
||||
help='config files to use, colon seperated, later files override')
|
||||
self.add_argument(
|
||||
'-o', '--option',
|
||||
@ -251,22 +257,26 @@ class RootCommand(Command):
|
||||
type=config_override_type, metavar='OPTIONS',
|
||||
help='`section/key=value` values to override config options')
|
||||
|
||||
def run(self, args, config, extensions):
|
||||
def run(self, args, config):
|
||||
loop = gobject.MainLoop()
|
||||
|
||||
backend_classes = args.registry['backend']
|
||||
frontend_classes = args.registry['frontend']
|
||||
|
||||
try:
|
||||
audio = self.start_audio(config)
|
||||
backends = self.start_backends(config, extensions, audio)
|
||||
backends = self.start_backends(config, backend_classes, audio)
|
||||
core = self.start_core(audio, backends)
|
||||
self.start_frontends(config, extensions, core)
|
||||
self.start_frontends(config, frontend_classes, core)
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Interrupted. Exiting...')
|
||||
return
|
||||
finally:
|
||||
loop.quit()
|
||||
self.stop_frontends(extensions)
|
||||
self.stop_frontends(frontend_classes)
|
||||
self.stop_core()
|
||||
self.stop_backends(extensions)
|
||||
self.stop_backends(backend_classes)
|
||||
self.stop_audio()
|
||||
process.stop_remaining_actors()
|
||||
|
||||
@ -274,11 +284,7 @@ class RootCommand(Command):
|
||||
logger.info('Starting Mopidy audio')
|
||||
return Audio.start(config=config).proxy()
|
||||
|
||||
def start_backends(self, config, extensions, audio):
|
||||
backend_classes = []
|
||||
for extension in extensions:
|
||||
backend_classes.extend(extension.get_backend_classes())
|
||||
|
||||
def start_backends(self, config, backend_classes, audio):
|
||||
logger.info(
|
||||
'Starting Mopidy backends: %s',
|
||||
', '.join(b.__name__ for b in backend_classes) or 'none')
|
||||
@ -294,11 +300,7 @@ class RootCommand(Command):
|
||||
logger.info('Starting Mopidy core')
|
||||
return Core.start(audio=audio, backends=backends).proxy()
|
||||
|
||||
def start_frontends(self, config, extensions, core):
|
||||
frontend_classes = []
|
||||
for extension in extensions:
|
||||
frontend_classes.extend(extension.get_frontend_classes())
|
||||
|
||||
def start_frontends(self, config, frontend_classes, core):
|
||||
logger.info(
|
||||
'Starting Mopidy frontends: %s',
|
||||
', '.join(f.__name__ for f in frontend_classes) or 'none')
|
||||
@ -306,21 +308,19 @@ class RootCommand(Command):
|
||||
for frontend_class in frontend_classes:
|
||||
frontend_class.start(config=config, core=core)
|
||||
|
||||
def stop_frontends(self, extensions):
|
||||
def stop_frontends(self, frontend_classes):
|
||||
logger.info('Stopping Mopidy frontends')
|
||||
for extension in extensions:
|
||||
for frontend_class in extension.get_frontend_classes():
|
||||
process.stop_actors_by_class(frontend_class)
|
||||
for frontend_class in frontend_classes:
|
||||
process.stop_actors_by_class(frontend_class)
|
||||
|
||||
def stop_core(self):
|
||||
logger.info('Stopping Mopidy core')
|
||||
process.stop_actors_by_class(Core)
|
||||
|
||||
def stop_backends(self, extensions):
|
||||
def stop_backends(self, backend_classes):
|
||||
logger.info('Stopping Mopidy backends')
|
||||
for extension in extensions:
|
||||
for backend_class in extension.get_backend_classes():
|
||||
process.stop_actors_by_class(backend_class)
|
||||
for backend_class in backend_classes:
|
||||
process.stop_actors_by_class(backend_class)
|
||||
|
||||
def stop_audio(self):
|
||||
logger.info('Stopping Mopidy audio')
|
||||
@ -335,7 +335,7 @@ class ConfigCommand(Command):
|
||||
self.set(base_verbosity_level=-1)
|
||||
|
||||
def run(self, config, errors, extensions):
|
||||
print config_lib.format(config, extensions, errors)
|
||||
print(config_lib.format(config, extensions, errors))
|
||||
return 0
|
||||
|
||||
|
||||
@ -347,5 +347,5 @@ class DepsCommand(Command):
|
||||
self.set(base_verbosity_level=-1)
|
||||
|
||||
def run(self):
|
||||
print deps.format_dependency_list()
|
||||
print(deps.format_dependency_list())
|
||||
return 0
|
||||
|
||||
@ -12,7 +12,7 @@ from mopidy.config.schemas import * # noqa
|
||||
from mopidy.config.types import * # noqa
|
||||
from mopidy.utils import path, versioning
|
||||
|
||||
logger = logging.getLogger('mopidy.config')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_logging_schema = ConfigSchema('logging')
|
||||
_logging_schema['console_format'] = String()
|
||||
@ -25,6 +25,7 @@ _loglevels_schema = LogLevelConfigSchema('loglevels')
|
||||
_audio_schema = ConfigSchema('audio')
|
||||
_audio_schema['mixer'] = String()
|
||||
_audio_schema['mixer_track'] = String(optional=True)
|
||||
_audio_schema['mixer_volume'] = Integer(optional=True, minimum=0, maximum=100)
|
||||
_audio_schema['output'] = String()
|
||||
_audio_schema['visualizer'] = String(optional=True)
|
||||
|
||||
@ -119,8 +120,8 @@ def _load(files, defaults, overrides):
|
||||
with io.open(filename, 'rb') as filehandle:
|
||||
parser.readfp(filehandle)
|
||||
except configparser.MissingSectionHeaderError as e:
|
||||
logging.warning('%s does not have a config section, not loaded.',
|
||||
filename)
|
||||
logger.warning('%s does not have a config section, not loaded.',
|
||||
filename)
|
||||
except configparser.ParsingError as e:
|
||||
linenos = ', '.join(str(lineno) for lineno, line in e.errors)
|
||||
logger.warning(
|
||||
@ -167,6 +168,8 @@ def _format(config, comments, schemas, display, disable):
|
||||
continue
|
||||
output.append(b'[%s]' % bytes(schema.name))
|
||||
for key, value in serialized.items():
|
||||
if isinstance(value, types.DeprecatedValue):
|
||||
continue
|
||||
comment = bytes(comments.get(schema.name, {}).get(key, ''))
|
||||
output.append(b'%s =' % bytes(key))
|
||||
if value is not None:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import os.path
|
||||
@ -10,13 +10,13 @@ from mopidy.utils import path
|
||||
|
||||
def load():
|
||||
settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
|
||||
print 'Checking %s' % settings_file
|
||||
print('Checking %s' % settings_file)
|
||||
|
||||
setting_globals = {}
|
||||
try:
|
||||
execfile(settings_file, setting_globals)
|
||||
except Exception as e:
|
||||
print 'Problem loading settings: %s' % e
|
||||
print('Problem loading settings: %s' % e)
|
||||
return setting_globals
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ def convert(settings):
|
||||
|
||||
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')
|
||||
@ -45,7 +46,6 @@ def convert(settings):
|
||||
|
||||
helper('local/media_dir', 'LOCAL_MUSIC_PATH')
|
||||
helper('local/playlists_dir', 'LOCAL_PLAYLIST_PATH')
|
||||
helper('local/tag_cache_file', 'LOCAL_TAG_CACHE_FILE')
|
||||
|
||||
helper('spotify/username', 'SPOTIFY_USERNAME')
|
||||
helper('spotify/password', 'SPOTIFY_PASSWORD')
|
||||
@ -107,20 +107,20 @@ def main():
|
||||
'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)
|
||||
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
|
||||
print('%s exists, exiting.' % conf_file)
|
||||
sys.exit(1)
|
||||
|
||||
print 'Write new config to %s? [yN]' % conf_file,
|
||||
print('Write new config to %s? [yN]' % conf_file, end=' ')
|
||||
if raw_input() != 'y':
|
||||
print 'Not saving, exiting.'
|
||||
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.'
|
||||
print('Done.')
|
||||
|
||||
@ -4,12 +4,10 @@ debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s
|
||||
debug_file = mopidy.log
|
||||
config_file =
|
||||
|
||||
[loglevels]
|
||||
pykka = info
|
||||
|
||||
[audio]
|
||||
mixer = software
|
||||
mixer_track =
|
||||
mixer_volume =
|
||||
output = autoaudiosink
|
||||
visualizer =
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.config.keyring')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import dbus
|
||||
|
||||
@ -54,7 +54,8 @@ class ConfigSchema(collections.OrderedDict):
|
||||
def deserialize(self, values):
|
||||
"""Validates the given ``values`` using the config schema.
|
||||
|
||||
Returns a tuple with cleaned values and errors."""
|
||||
Returns a tuple with cleaned values and errors.
|
||||
"""
|
||||
errors = {}
|
||||
result = {}
|
||||
|
||||
@ -71,7 +72,9 @@ class ConfigSchema(collections.OrderedDict):
|
||||
errors[key] = str(e)
|
||||
|
||||
for key in self.keys():
|
||||
if key not in result and key not in errors:
|
||||
if isinstance(self[key], types.Deprecated):
|
||||
result.pop(key, None)
|
||||
elif key not in result and key not in errors:
|
||||
result[key] = None
|
||||
errors[key] = 'config key not found.'
|
||||
|
||||
|
||||
@ -31,6 +31,10 @@ class ExpandedPath(bytes):
|
||||
self.original = original
|
||||
|
||||
|
||||
class DeprecatedValue(object):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigValue(object):
|
||||
"""Represents a config key's value and how to handle it.
|
||||
|
||||
@ -59,6 +63,20 @@ class ConfigValue(object):
|
||||
return bytes(value)
|
||||
|
||||
|
||||
class Deprecated(ConfigValue):
|
||||
"""Deprecated value
|
||||
|
||||
Used for ignoring old config values that are no longer in use, but should
|
||||
not cause the config parser to crash.
|
||||
"""
|
||||
|
||||
def deserialize(self, value):
|
||||
return DeprecatedValue()
|
||||
|
||||
def serialize(self, value, display=False):
|
||||
return DeprecatedValue()
|
||||
|
||||
|
||||
class String(ConfigValue):
|
||||
"""String value.
|
||||
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.audio import AudioListener, PlaybackState
|
||||
from mopidy.backends.listener import BackendListener
|
||||
from mopidy import audio, backend
|
||||
from mopidy.audio import PlaybackState
|
||||
from mopidy.utils import versioning
|
||||
|
||||
from .library import LibraryController
|
||||
from .listener import CoreListener
|
||||
@ -14,22 +16,22 @@ from .playlists import PlaylistsController
|
||||
from .tracklist import TracklistController
|
||||
|
||||
|
||||
class Core(pykka.ThreadingActor, AudioListener, BackendListener):
|
||||
#: The library controller. An instance of
|
||||
# :class:`mopidy.core.LibraryController`.
|
||||
class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener):
|
||||
library = None
|
||||
"""The library controller. An instance of
|
||||
:class:`mopidy.core.LibraryController`."""
|
||||
|
||||
#: The playback controller. An instance of
|
||||
#: :class:`mopidy.core.PlaybackController`.
|
||||
playback = None
|
||||
"""The playback controller. An instance of
|
||||
:class:`mopidy.core.PlaybackController`."""
|
||||
|
||||
#: The playlists controller. An instance of
|
||||
#: :class:`mopidy.core.PlaylistsController`.
|
||||
playlists = None
|
||||
"""The playlists controller. An instance of
|
||||
:class:`mopidy.core.PlaylistsController`."""
|
||||
|
||||
#: The tracklist controller. An instance of
|
||||
#: :class:`mopidy.core.TracklistController`.
|
||||
tracklist = None
|
||||
"""The tracklist controller. An instance of
|
||||
:class:`mopidy.core.TracklistController`."""
|
||||
|
||||
def __init__(self, audio=None, backends=None):
|
||||
super(Core, self).__init__()
|
||||
@ -55,6 +57,12 @@ class Core(pykka.ThreadingActor, AudioListener, BackendListener):
|
||||
uri_schemes = property(get_uri_schemes)
|
||||
"""List of URI schemes we can handle"""
|
||||
|
||||
def get_version(self):
|
||||
return versioning.get_version()
|
||||
|
||||
version = property(get_version)
|
||||
"""Version of the Mopidy core API"""
|
||||
|
||||
def reached_end_of_stream(self):
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
@ -79,34 +87,32 @@ class Backends(list):
|
||||
def __init__(self, backends):
|
||||
super(Backends, self).__init__(backends)
|
||||
|
||||
# These lists keeps the backends in the original order, but only
|
||||
# includes those which implements the required backend provider. Since
|
||||
# it is important to keep the order, we can't simply use .values() on
|
||||
# the X_by_uri_scheme dicts below.
|
||||
self.with_library = [b for b in backends if b.has_library().get()]
|
||||
self.with_playback = [b for b in backends if b.has_playback().get()]
|
||||
self.with_playlists = [
|
||||
b for b in backends if b.has_playlists().get()]
|
||||
self.with_library = collections.OrderedDict()
|
||||
self.with_library_browse = collections.OrderedDict()
|
||||
self.with_playback = collections.OrderedDict()
|
||||
self.with_playlists = collections.OrderedDict()
|
||||
|
||||
backends_by_scheme = {}
|
||||
name = lambda backend: backend.actor_ref.actor_class.__name__
|
||||
|
||||
self.by_uri_scheme = {}
|
||||
for backend in backends:
|
||||
for uri_scheme in backend.uri_schemes.get():
|
||||
assert uri_scheme not in self.by_uri_scheme, (
|
||||
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 scheme in backend.uri_schemes.get():
|
||||
assert scheme not in backends_by_scheme, (
|
||||
'Cannot add URI scheme %s for %s, '
|
||||
'it is already handled by %s'
|
||||
) % (
|
||||
uri_scheme, backend.__class__.__name__,
|
||||
self.by_uri_scheme[uri_scheme].__class__.__name__)
|
||||
self.by_uri_scheme[uri_scheme] = backend
|
||||
) % (scheme, name(backend), name(backends_by_scheme[scheme]))
|
||||
backends_by_scheme[scheme] = backend
|
||||
|
||||
self.with_library_by_uri_scheme = {}
|
||||
self.with_playback_by_uri_scheme = {}
|
||||
self.with_playlists_by_uri_scheme = {}
|
||||
|
||||
for uri_scheme, backend in self.by_uri_scheme.items():
|
||||
if backend.has_library().get():
|
||||
self.with_library_by_uri_scheme[uri_scheme] = backend
|
||||
if backend.has_playback().get():
|
||||
self.with_playback_by_uri_scheme[uri_scheme] = backend
|
||||
if backend.has_playlists().get():
|
||||
self.with_playlists_by_uri_scheme[uri_scheme] = backend
|
||||
if has_library:
|
||||
self.with_library[scheme] = backend
|
||||
if has_library_browse:
|
||||
self.with_library_browse[scheme] = backend
|
||||
if has_playback:
|
||||
self.with_playback[scheme] = backend
|
||||
if has_playlists:
|
||||
self.with_playlists[scheme] = backend
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
import collections
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
@ -15,20 +15,61 @@ class LibraryController(object):
|
||||
|
||||
def _get_backend(self, uri):
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.with_library_by_uri_scheme.get(uri_scheme, None)
|
||||
return self.backends.with_library.get(uri_scheme, None)
|
||||
|
||||
def _get_backends_to_uris(self, uris):
|
||||
if uris:
|
||||
backends_to_uris = defaultdict(list)
|
||||
backends_to_uris = collections.defaultdict(list)
|
||||
for uri in uris:
|
||||
backend = self._get_backend(uri)
|
||||
if backend is not None:
|
||||
backends_to_uris[backend].append(uri)
|
||||
else:
|
||||
backends_to_uris = dict([
|
||||
(b, None) for b in self.backends.with_library])
|
||||
(b, None) for b in self.backends.with_library.values()])
|
||||
return backends_to_uris
|
||||
|
||||
def browse(self, uri):
|
||||
"""
|
||||
Browse directories and tracks at the given ``uri``.
|
||||
|
||||
``uri`` is a string which represents some directory belonging to a
|
||||
backend. To get the intial root directories for backends pass None as
|
||||
the URI.
|
||||
|
||||
Returns a list of :class:`mopidy.models.Ref` objects for the
|
||||
directories and tracks at the given ``uri``.
|
||||
|
||||
The :class:`~mopidy.models.Ref` objects representing tracks keep the
|
||||
track's original URI. A matching pair of objects can look like this::
|
||||
|
||||
Track(uri='dummy:/foo.mp3', name='foo', artists=..., album=...)
|
||||
Ref.track(uri='dummy:/foo.mp3', name='foo')
|
||||
|
||||
The :class:`~mopidy.models.Ref` objects representing directories have
|
||||
backend specific URIs. These are opaque values, so no one but the
|
||||
backend that created them should try and derive any meaning from them.
|
||||
The only valid exception to this is checking the scheme, as it is used
|
||||
to route browse requests to the correct backend.
|
||||
|
||||
For example, the dummy library's ``/bar`` directory could be returned
|
||||
like this::
|
||||
|
||||
Ref.directory(uri='dummy:directory:/bar', name='bar')
|
||||
|
||||
:param string uri: URI to browse
|
||||
:rtype: list of :class:`mopidy.models.Ref`
|
||||
"""
|
||||
if uri is None:
|
||||
backends = self.backends.with_library_browse.values()
|
||||
return [b.library.root_directory.get() for b in backends]
|
||||
|
||||
scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_library_browse.get(scheme)
|
||||
if not backend:
|
||||
return []
|
||||
return backend.library.browse(uri).get()
|
||||
|
||||
def find_exact(self, query=None, uris=None, **kwargs):
|
||||
"""
|
||||
Search the library for tracks where ``field`` is ``values``.
|
||||
@ -103,8 +144,8 @@ class LibraryController(object):
|
||||
if backend:
|
||||
backend.library.refresh(uri).get()
|
||||
else:
|
||||
futures = [
|
||||
b.library.refresh(uri) for b in self.backends.with_library]
|
||||
futures = [b.library.refresh(uri)
|
||||
for b in self.backends.with_library.values()]
|
||||
pykka.get_all(futures)
|
||||
|
||||
def search(self, query=None, uris=None, **kwargs):
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
from mopidy import listener
|
||||
|
||||
|
||||
class CoreListener(object):
|
||||
class CoreListener(listener.Listener):
|
||||
"""
|
||||
Marker interface for recipients of events sent by the core actor.
|
||||
|
||||
@ -17,9 +17,7 @@ class CoreListener(object):
|
||||
@staticmethod
|
||||
def send(event, **kwargs):
|
||||
"""Helper to allow calling of core listener events"""
|
||||
listeners = pykka.ActorRegistry.get_by_class(CoreListener)
|
||||
for listener in listeners:
|
||||
listener.proxy().on_event(event, **kwargs)
|
||||
listener.send_async(CoreListener, event, **kwargs)
|
||||
|
||||
def on_event(self, event, **kwargs):
|
||||
"""
|
||||
|
||||
@ -8,7 +8,7 @@ from mopidy.audio import PlaybackState
|
||||
from . import listener
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlaybackController(object):
|
||||
@ -28,7 +28,7 @@ class PlaybackController(object):
|
||||
return None
|
||||
uri = self.current_tl_track.track.uri
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None)
|
||||
return self.backends.with_playback.get(uri_scheme, None)
|
||||
|
||||
### Properties
|
||||
|
||||
@ -160,8 +160,7 @@ class PlaybackController(object):
|
||||
next_tl_track = self.core.tracklist.eot_track(original_tl_track)
|
||||
|
||||
if next_tl_track:
|
||||
self._trigger_track_playback_ended()
|
||||
self.play(next_tl_track)
|
||||
self.change_track(next_tl_track)
|
||||
else:
|
||||
self.stop(clear_current_track=True)
|
||||
|
||||
@ -185,7 +184,6 @@ class PlaybackController(object):
|
||||
"""
|
||||
tl_track = self.core.tracklist.next_track(self.current_tl_track)
|
||||
if tl_track:
|
||||
self._trigger_track_playback_ended()
|
||||
self.change_track(tl_track)
|
||||
else:
|
||||
self.stop(clear_current_track=True)
|
||||
@ -228,6 +226,9 @@ class PlaybackController(object):
|
||||
|
||||
assert tl_track in self.core.tracklist.tl_tracks
|
||||
|
||||
if self.state == PlaybackState.PLAYING:
|
||||
self.stop()
|
||||
|
||||
self.current_tl_track = tl_track
|
||||
self.state = PlaybackState.PLAYING
|
||||
backend = self._get_backend()
|
||||
@ -251,7 +252,6 @@ class PlaybackController(object):
|
||||
The current playback state will be kept. If it was playing, playing
|
||||
will continue. If it was paused, it will still be paused, etc.
|
||||
"""
|
||||
self._trigger_track_playback_ended()
|
||||
tl_track = self.current_tl_track
|
||||
self.change_track(
|
||||
self.core.tracklist.previous_track(tl_track), on_error_step=-1)
|
||||
@ -307,8 +307,8 @@ class PlaybackController(object):
|
||||
if self.state != PlaybackState.STOPPED:
|
||||
backend = self._get_backend()
|
||||
if not backend or backend.playback.stop().get():
|
||||
self._trigger_track_playback_ended()
|
||||
self.state = PlaybackState.STOPPED
|
||||
self._trigger_track_playback_ended()
|
||||
if clear_current_track:
|
||||
self.current_tl_track = None
|
||||
|
||||
|
||||
@ -16,8 +16,8 @@ class PlaylistsController(object):
|
||||
self.core = core
|
||||
|
||||
def get_playlists(self, include_tracks=True):
|
||||
futures = [
|
||||
b.playlists.playlists for b in self.backends.with_playlists]
|
||||
futures = [b.playlists.playlists
|
||||
for b in self.backends.with_playlists.values()]
|
||||
results = pykka.get_all(futures)
|
||||
playlists = list(itertools.chain(*results))
|
||||
if not include_tracks:
|
||||
@ -49,10 +49,11 @@ class PlaylistsController(object):
|
||||
:type uri_scheme: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
if uri_scheme in self.backends.with_playlists_by_uri_scheme:
|
||||
backend = self.backends.by_uri_scheme[uri_scheme]
|
||||
if uri_scheme in self.backends.with_playlists:
|
||||
backend = self.backends.with_playlists[uri_scheme]
|
||||
else:
|
||||
backend = self.backends.with_playlists[0]
|
||||
# TODO: this fallback looks suspicious
|
||||
backend = self.backends.with_playlists.values()[0]
|
||||
playlist = backend.playlists.create(name).get()
|
||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
||||
return playlist
|
||||
@ -68,8 +69,7 @@ class PlaylistsController(object):
|
||||
:type uri: string
|
||||
"""
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
backend.playlists.delete(uri).get()
|
||||
|
||||
@ -111,8 +111,7 @@ class PlaylistsController(object):
|
||||
:rtype: :class:`mopidy.models.Playlist` or :class:`None`
|
||||
"""
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
return backend.playlists.lookup(uri).get()
|
||||
else:
|
||||
@ -131,13 +130,12 @@ class PlaylistsController(object):
|
||||
:type uri_scheme: string
|
||||
"""
|
||||
if uri_scheme is None:
|
||||
futures = [
|
||||
b.playlists.refresh() for b in self.backends.with_playlists]
|
||||
futures = [b.playlists.refresh()
|
||||
for b in self.backends.with_playlists.values()]
|
||||
pykka.get_all(futures)
|
||||
listener.CoreListener.send('playlists_loaded')
|
||||
else:
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
backend.playlists.refresh().get()
|
||||
listener.CoreListener.send('playlists_loaded')
|
||||
@ -167,8 +165,7 @@ class PlaylistsController(object):
|
||||
if playlist.uri is None:
|
||||
return
|
||||
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
playlist = backend.playlists.save(playlist).get()
|
||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
||||
|
||||
@ -9,7 +9,7 @@ from mopidy.models import TlTrack
|
||||
from . import listener
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TracklistController(object):
|
||||
|
||||
165
mopidy/ext.py
@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import pkg_resources
|
||||
|
||||
@ -7,7 +8,7 @@ from mopidy import exceptions
|
||||
from mopidy import config as config_lib
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.ext')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(object):
|
||||
@ -50,43 +51,6 @@ class Extension(object):
|
||||
schema['enabled'] = config_lib.Boolean()
|
||||
return schema
|
||||
|
||||
def validate_environment(self):
|
||||
"""Checks if the extension can run in the current environment
|
||||
|
||||
For example, this method can be used to check if all dependencies that
|
||||
are needed are installed.
|
||||
|
||||
:raises: :class:`~mopidy.exceptions.ExtensionError`
|
||||
:returns: :class:`None`
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_frontend_classes(self):
|
||||
"""List of frontend actor classes
|
||||
|
||||
Mopidy will take care of starting the actors.
|
||||
|
||||
:returns: list of :class:`pykka.Actor` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_backend_classes(self):
|
||||
"""List of backend actor classes
|
||||
|
||||
Mopidy will take care of starting the actors.
|
||||
|
||||
:returns: list of :class:`~mopidy.backends.base.Backend` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_library_updaters(self):
|
||||
"""List of library updater classes
|
||||
|
||||
:returns: list of
|
||||
:class:`~mopidy.backends.base.BaseLibraryUpdateProvider` subclasses
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_command(self):
|
||||
"""Command to expose to command line users running mopidy.
|
||||
|
||||
@ -95,23 +59,124 @@ class Extension(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
"""Hook for registering custom GStreamer elements
|
||||
def validate_environment(self):
|
||||
"""Checks if the extension can run in the current environment
|
||||
|
||||
Register custom GStreamer elements by implementing this method.
|
||||
Example::
|
||||
For example, this method can be used to check if all dependencies that
|
||||
are needed are installed. If a problem is found, raise
|
||||
:exc:`~mopidy.exceptions.ExtensionError` with a message explaining the
|
||||
issue.
|
||||
|
||||
def register_gstreamer_elements(self):
|
||||
:raises: :exc:`~mopidy.exceptions.ExtensionError`
|
||||
:returns: :class:`None`
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup(self, registry):
|
||||
"""
|
||||
Register the extension's components in the extension :class:`Registry`.
|
||||
|
||||
For example, to register a backend::
|
||||
|
||||
def setup(self, registry):
|
||||
from .backend import SoundspotBackend
|
||||
registry.add('backend', SoundspotBackend)
|
||||
|
||||
See :class:`Registry` for a list of registry keys with a special
|
||||
meaning. Mopidy will instantiate and start any classes registered under
|
||||
the ``frontend`` and ``backend`` registry keys.
|
||||
|
||||
This method can also be used for other setup tasks not involving the
|
||||
extension registry. For example, to register custom GStreamer
|
||||
elements::
|
||||
|
||||
def setup(self, registry):
|
||||
from .mixer import SoundspotMixer
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||
|
||||
: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
|
||||
|
||||
|
||||
class Registry(collections.Mapping):
|
||||
"""Registry of components provided by Mopidy extensions.
|
||||
|
||||
Passed to the :meth:`~Extension.setup` method of all extensions. The
|
||||
registry can be used like a dict of string keys and lists.
|
||||
|
||||
Some keys have a special meaning, including, but not limited to:
|
||||
|
||||
- ``backend`` is used for Mopidy backend classes.
|
||||
- ``frontend`` is used for Mopidy frontend classes.
|
||||
- ``local:library`` is used for Mopidy-Local libraries.
|
||||
|
||||
Extensions can use the registry for allow other to extend the extension
|
||||
itself. For example the ``Mopidy-Local`` use the ``local:library`` key to
|
||||
allow other extensions to register library providers for ``Mopidy-Local``
|
||||
to use. Extensions should namespace custom keys with the extension's
|
||||
:attr:`~Extension.ext_name`, e.g. ``local:foo`` or ``http:bar``.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
|
||||
def add(self, name, cls):
|
||||
"""Add a component to the registry.
|
||||
|
||||
Multiple classes can be registered to the same name.
|
||||
"""
|
||||
self._registry.setdefault(name, []).append(cls)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._registry.setdefault(name, [])
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._registry)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._registry)
|
||||
|
||||
|
||||
def load_extensions():
|
||||
"""Find all installed extensions.
|
||||
|
||||
@ -130,7 +195,7 @@ def load_extensions():
|
||||
'Loaded extension: %s %s', extension.dist_name, extension.version)
|
||||
|
||||
names = (e.ext_name for e in installed_extensions)
|
||||
logging.debug('Discovered extensions: %s', ', '.join(names))
|
||||
logger.debug('Discovered extensions: %s', ', '.join(names))
|
||||
return installed_extensions
|
||||
|
||||
|
||||
@ -166,15 +231,3 @@ def validate_extension(extension):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register_gstreamer_elements(enabled_extensions):
|
||||
"""Registers custom GStreamer elements from extensions.
|
||||
|
||||
:param enabled_extensions: list of enabled extensions
|
||||
"""
|
||||
|
||||
for extension in enabled_extensions:
|
||||
logger.debug(
|
||||
'Registering GStreamer elements for: %s', extension.ext_name)
|
||||
extension.register_gstreamer_elements()
|
||||
|
||||
5
mopidy/frontends/http/data/mopidy.min.js
vendored
@ -1,9 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
|
||||
|
||||
@handle_request(r'[\ ]*$')
|
||||
def empty(context):
|
||||
"""The original MPD server returns ``OK`` on an empty request."""
|
||||
pass
|
||||
@ -35,6 +35,6 @@ class Extension(ext.Extension):
|
||||
except ImportError as e:
|
||||
raise exceptions.ExtensionError('ws4py library not found', e)
|
||||
|
||||
def get_frontend_classes(self):
|
||||
def setup(self, registry):
|
||||
from .actor import HttpFrontend
|
||||
return [HttpFrontend]
|
||||
registry.add('frontend', HttpFrontend)
|
||||
@ -9,13 +9,12 @@ import pykka
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
|
||||
from mopidy import models
|
||||
from mopidy import models, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.utils import zeroconf
|
||||
from . import ws
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.http')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
@ -104,7 +103,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
logger.info('Registered HTTP with Zeroconf as "%s"',
|
||||
self.zeroconf_service.name)
|
||||
else:
|
||||
logger.warning('Registering HTTP with Zeroconf failed.')
|
||||
logger.info('Registering HTTP with Zeroconf failed.')
|
||||
|
||||
def on_stop(self):
|
||||
if self.zeroconf_service:
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |