Release v0.18.0

This commit is contained in:
Stein Magnus Jodal 2014-01-19 22:30:08 +01:00
commit 809e48b5dc
243 changed files with 5283 additions and 5817 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ node_modules/
nosetests.xml
*~
*.orig
js/test/lib/

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,11 @@
.. _zeroconf-api:
************
Zeroconf API
************
.. module:: mopidy.zeroconf
:synopsis: Helper for publishing of services on Zeroconf
.. autoclass:: Zeroconf
:members:

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 519 KiB

View File

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

View File

@ -1,8 +0,0 @@
*******
Clients
*******
.. toctree::
:glob:
**

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 284 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

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

View File

@ -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#'),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
module.exports = { Client: window.WebSocket };

View File

@ -0,0 +1,4 @@
{
"browser": "browser.js",
"main": "server.js"
}

View File

@ -0,0 +1 @@
module.exports = require('faye-websocket');

View File

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

View File

@ -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 = {};
}

View File

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

View File

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

View File

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

View File

@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.17.0'
__version__ = '0.18.0'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import logging
logger = logging.getLogger('mopidy.config.keyring')
logger = logging.getLogger(__name__)
try:
import dbus

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ from mopidy.models import TlTrack
from . import listener
logger = logging.getLogger('mopidy.core')
logger = logging.getLogger(__name__)
class TracklistController(object):

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Some files were not shown because too many files have changed in this diff Show More