Release v0.13.0
This commit is contained in:
commit
5d83e3e97a
5
AUTHORS
5
AUTHORS
@ -13,3 +13,8 @@
|
||||
- Matt Bray <mattjbray@gmail.com>
|
||||
- Trygve Aaberge <trygveaa@gmail.com>
|
||||
- Wouter van Wijk <woutervanwijk@gmail.com>
|
||||
- Jeremy B. Merrill <jeremybmerrill@gmail.com>
|
||||
- 0xadam <radx@live.com.au>
|
||||
- herrernst <herr.ernst@gmail.com>
|
||||
- Nick Steel <kingosticks@gmail.com>
|
||||
- Zan Dobersek <zandobersek@gmail.com>
|
||||
|
||||
@ -22,4 +22,5 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
|
||||
- `CI server <http://travis-ci.org/mopidy/mopidy>`_
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
- `Download development snapshot <http://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
|
||||
BIN
docs/_static/dz0ny-mopidy-lux.png
vendored
Normal file
BIN
docs/_static/dz0ny-mopidy-lux.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 519 KiB |
BIN
docs/_static/woutervanwijk-mopidy-webclient.png
vendored
BIN
docs/_static/woutervanwijk-mopidy-webclient.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
@ -18,6 +18,10 @@ Data model relations
|
||||
Track -> Artist [ label="has 0..n" ]
|
||||
Album -> Artist [ label="has 0..n" ]
|
||||
|
||||
SearchResult -> Artist [ label="has 0..n" ]
|
||||
SearchResult -> Album [ label="has 0..n" ]
|
||||
SearchResult -> Track [ label="has 0..n" ]
|
||||
|
||||
|
||||
Data model API
|
||||
==============
|
||||
|
||||
@ -5,6 +5,86 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
v0.13.0 (2013-03-31)
|
||||
====================
|
||||
|
||||
The 0.13 release brings small improvements and bugfixes throughout Mopidy.
|
||||
There are no major new features, just incremental improvement of what we
|
||||
already have.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- Pykka >= 1.1 is now required.
|
||||
|
||||
**Core**
|
||||
|
||||
- Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the
|
||||
:option:`--debug-thread` command line option. Sending SIGUSR1 to
|
||||
the Mopidy process will now always make it log tracebacks for all alive
|
||||
threads.
|
||||
|
||||
- Log a warning if a track isn't playable to make it more obvious that backend
|
||||
X needs backend Y to be present for playback to work.
|
||||
|
||||
- :meth:`mopidy.core.TracklistController.add` now accepts an ``uri`` which it
|
||||
will lookup in the library and then add to the tracklist. This is helpful
|
||||
for e.g. web clients that doesn't want to transfer all track meta data back
|
||||
to the server just to add it to the tracklist when the server already got all
|
||||
the needed information easily available. (Fixes: :issue:`325`)
|
||||
|
||||
- Change the following methods to accept an ``uris`` keyword argument:
|
||||
|
||||
- :meth:`mopidy.core.LibraryController.find_exact`
|
||||
- :meth:`mopidy.core.LibraryController.search`
|
||||
|
||||
Search queries will only be forwarded to backends handling the given URI
|
||||
roots, and the backends may use the URI roots to further limit what results
|
||||
are returned. For example, a search with ``uris=['file:']`` will only be
|
||||
processed by the local backend. A search with
|
||||
``uris=['file:///media/music']`` will only be processed by the local backend,
|
||||
and, if such filtering is supported by the backend, will only return results
|
||||
with URIs within the given URI root.
|
||||
|
||||
**Audio sub-system**
|
||||
|
||||
- Make audio error logging handle log messages with non-ASCII chars. (Fixes:
|
||||
:issue:`347`)
|
||||
|
||||
**Local backend**
|
||||
|
||||
- Make ``mopidy-scan`` work with Ogg Vorbis files. (Fixes: :issue:`275`)
|
||||
|
||||
- Fix playback of files with non-ASCII chars in their file path. (Fixes:
|
||||
:issue:`353`)
|
||||
|
||||
**Spotify backend**
|
||||
|
||||
- Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`)
|
||||
|
||||
- For all playlists owned by other Spotify users, we now append the owner's
|
||||
username to the playlist name. (Partly fixes: :issue:`114`)
|
||||
|
||||
**HTTP frontend**
|
||||
|
||||
- Mopidy.js now works both from browsers and from Node.js environments. This
|
||||
means that you now can make Mopidy clients in Node.js. Mopidy.js has been
|
||||
published to the `npm registry <https://npmjs.org/package/mopidy>`_ for easy
|
||||
installation in Node.js projects.
|
||||
|
||||
- Upgrade Mopidy.js' build system Grunt from 0.3 to 0.4.
|
||||
|
||||
- Upgrade Mopidy.js' dependencies when.js from 1.6.1 to 2.0.0.
|
||||
|
||||
- Expose :meth:`mopidy.core.Core.get_uri_schemes` to HTTP clients. It is
|
||||
available through Mopidy.js as ``mopidy.getUriSchemes()``.
|
||||
|
||||
**MPRIS frontend**
|
||||
|
||||
- Publish album art URIs if available.
|
||||
|
||||
- Publish disc number of track if available.
|
||||
|
||||
|
||||
v0.12.0 (2013-03-12)
|
||||
====================
|
||||
|
||||
|
||||
@ -18,8 +18,8 @@ woutervanwijk/Mopidy-Webclient
|
||||
==============================
|
||||
|
||||
.. image:: /_static/woutervanwijk-mopidy-webclient.png
|
||||
:width: 410
|
||||
:height: 511
|
||||
:width: 382
|
||||
:height: 621
|
||||
|
||||
The first web client for Mopidy is still under development, but is already very
|
||||
usable. It targets both desktop and mobile browsers.
|
||||
@ -29,6 +29,24 @@ and point the :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` setting towards
|
||||
your copy of the web client.
|
||||
|
||||
|
||||
Mopidy Lux
|
||||
==========
|
||||
|
||||
.. image:: /_static/dz0ny-mopidy-lux.png
|
||||
:width: 1000
|
||||
:height: 645
|
||||
|
||||
New web client developed by Janez Troha. See
|
||||
https://github.com/dz0ny/mopidy-lux for details.
|
||||
|
||||
|
||||
JukePi
|
||||
======
|
||||
|
||||
New web client developed by Meantime IT in the UK for their office jukebox. See
|
||||
https://github.com/meantimeit/jukepi for details.
|
||||
|
||||
|
||||
Rompr
|
||||
=====
|
||||
|
||||
|
||||
@ -305,10 +305,16 @@ Debugging deadlocks
|
||||
|
||||
Between the numerous Pykka threads and GStreamer interactions there can
|
||||
sometimes be a potential for deadlocks. In an effort to make these slightly
|
||||
simpler to debug the setting :attr:`mopidy.settings.DEBUG_THREAD` or the option
|
||||
``--debug-thread`` can be used to turn on an extra debug thread. This thread is
|
||||
not linked to the regular program flow, and it's only task is to dump traceback
|
||||
showing the other threads state when we get a ``SIGUSR1``.
|
||||
simpler to debug Mopidy registers a ``SIGUSR1`` signal handler which logs the
|
||||
traceback of all alive threads.
|
||||
|
||||
To trigger the signal handler, you can use the ``pkill`` command to
|
||||
send the ``SIGUSR1`` signal to any Mopidy processes::
|
||||
|
||||
pkill -SIGUSR1 mopidy
|
||||
|
||||
If you check the log, you should now find one log record with a full traceback
|
||||
for each of the currently alive threads in Mopidy.
|
||||
|
||||
|
||||
Writing documentation
|
||||
|
||||
410
docs/extensiondev.rst
Normal file
410
docs/extensiondev.rst
Normal file
@ -0,0 +1,410 @@
|
||||
*********************
|
||||
Extension development
|
||||
*********************
|
||||
|
||||
.. warning:: Draft
|
||||
|
||||
This document is a draft open for discussion. It shows how we imagine that
|
||||
development of Mopidy extensions should become in the future, not how to
|
||||
currently develop an extension for Mopidy.
|
||||
|
||||
|
||||
Mopidy started as simply an MPD server that could play music from Spotify.
|
||||
Early on Mopidy got multiple "frontends" to expose Mopidy to more than just MPD
|
||||
clients: for example the Last.fm frontend what scrobbles what you've listened
|
||||
to to your Last.fm account, the MPRIS frontend that integrates Mopidy into the
|
||||
Ubuntu Sound Menu, and the HTTP server and JavaScript player API making web
|
||||
based Mopidy clients possible. In Mopidy 0.9 we added support for multiple
|
||||
music sources without stopping and reconfiguring Mopidy: for example the local
|
||||
backend for playing music from your disk, the stream backend for playing
|
||||
Internet radio streams, and the Spotify and SoundCloud backends, for playing
|
||||
music directly from those services.
|
||||
|
||||
All of these are examples of what you can accomplish by creating a Mopidy
|
||||
extension. If you want to create your own Mopidy extension for something that
|
||||
does not exist yet, this guide to extension development will help you get your
|
||||
extension running in no time, and make it feel the way users would expect your
|
||||
extension to behave.
|
||||
|
||||
|
||||
Anatomy of an extension
|
||||
=======================
|
||||
|
||||
Extensions are all located in a Python package called ``mopidy_something``
|
||||
where "something" is the name of the application, library or web service you
|
||||
want to integrated with Mopidy. So for example if you plan to add support for a
|
||||
service named Soundspot to Mopidy, you would name your extension's Python
|
||||
package ``mopidy_soundspot``.
|
||||
|
||||
The name of the actual extension (the human readable name) however would be
|
||||
something like "Mopidy-Soundspot". Make sure to include the name "Mopidy"
|
||||
somewhere in that name and that you check the capitalization. This is the name
|
||||
users will use when they install your extension from PyPI.
|
||||
|
||||
The extension must be shipped with a ``setup.py`` file and be registered on
|
||||
`PyPI <https://pypi.python.org/>`_. Also make sure the development version link
|
||||
in your package details work so that people can easily install the development
|
||||
version into their virtualenv simply by running e.g. ``pip install
|
||||
Mopidy-Soundspot==dev``.
|
||||
|
||||
Mopidy extensions must be licensed under an Apache 2.0 (like Mopidy itself),
|
||||
BSD, MIT or more liberal license to be able to be enlisted in the Mopidy
|
||||
documentation. The license text should be included in the ``LICENSE`` file in
|
||||
the root of the extension's Git repo.
|
||||
|
||||
Combining this together, we get the following folder structure for our
|
||||
extension, Mopidy-Soundspot::
|
||||
|
||||
mopidy-soundspot/ # The Git repo root
|
||||
LICENSE # The license text
|
||||
README.rst # Document what it is and how to use it
|
||||
mopidy_soundspot/ # Your code
|
||||
__init__.py
|
||||
config.ini # Default configuration for the extension
|
||||
...
|
||||
setup.py # Installation script
|
||||
|
||||
Example content for the most important files follows below.
|
||||
|
||||
|
||||
Example README.rst
|
||||
==================
|
||||
|
||||
The README file should quickly tell what the extension does, how to install it,
|
||||
and how to configure it. The README should contain a development snapshot link
|
||||
to a tarball of the latest development version of the extension. It's important
|
||||
that the development snapshot link ends with ``#egg=mopidy-something-dev`` for
|
||||
installation using ``pip install mopidy-something==dev`` to work.
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
Mopidy-Soundspot
|
||||
================
|
||||
|
||||
`Mopidy <http://www.mopidy.com/>`_ extension for playing music from
|
||||
`Soundspot <http://soundspot.example.com/>`_.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Requires a Soundspot Platina subscription and the pysoundspot library.
|
||||
|
||||
Install by running::
|
||||
|
||||
sudo pip install Mopidy-Soundspot
|
||||
|
||||
Or install the Debian/Ubuntu package from `apt.mopidy.com
|
||||
<http://apt.mopidy.com/>`_.
|
||||
|
||||
Before starting Mopidy, you must add your Soundspot username and password
|
||||
to the Mopidy configuration file::
|
||||
|
||||
[soundspot]
|
||||
username = alice
|
||||
password = secret
|
||||
|
||||
Project resources
|
||||
-----------------
|
||||
|
||||
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy-soundspot/issues>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/develop#egg=mopidy-soundspot-dev>`_
|
||||
|
||||
|
||||
Example setup.py
|
||||
================
|
||||
|
||||
The ``setup.py`` file must use setuptools/distribute, and not distutils. This
|
||||
is because Mopidy extensions use setuptools' entry point functionality to
|
||||
register themselves as available Mopidy extensions when they are installed on
|
||||
your system.
|
||||
|
||||
The example below also includes a couple of convenient tricks for reading the
|
||||
package version from the source code so that it it's just defined in a single
|
||||
place, and to reuse the README file as the long description of the package for
|
||||
the PyPI registration.
|
||||
|
||||
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy``, in
|
||||
addition to any other dependencies required by your extension. The
|
||||
``entry_points`` part must be included. The ``mopidy.extension`` part cannot be
|
||||
changed, but the innermost string should be changed. It's format is
|
||||
``my_ext_name = my_py_module:MyExtClass``. ``my_ext_name`` should be a short
|
||||
name for your extension, typically the part after "Mopidy-" in lowercase. This
|
||||
name is used e.g. to name the config section for your extension. The
|
||||
``my_py_module:MyExtClass`` part is simply the Python path to the extension
|
||||
class that will connect the rest of the dots.
|
||||
|
||||
::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
def get_version(filename):
|
||||
content = open(filename).read()
|
||||
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content))
|
||||
return metadata['version']
|
||||
|
||||
|
||||
setup(
|
||||
name='Mopidy-Soundspot',
|
||||
version=get_version('mopidy_soundspot/__init__.py'),
|
||||
url='http://example.com/mopidy-soundspot/',
|
||||
license='Apache License, Version 2.0',
|
||||
author='Your Name',
|
||||
author_email='your-email@example.com',
|
||||
description='Very short description',
|
||||
long_description=open('README.rst').read(),
|
||||
packages=['mopidy_soundspot'],
|
||||
# If you ship package instead of a single module instead, use
|
||||
# 'py_modules' instead of 'packages':
|
||||
#py_modules=['mopidy_soundspot'],
|
||||
zip_safe=False,
|
||||
include_package_data=True,
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'setuptools',
|
||||
'Mopidy',
|
||||
'pysoundspot',
|
||||
],
|
||||
entry_points={
|
||||
'mopidy.extension': [
|
||||
'soundspot = mopidy_soundspot:Extension',
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
Example __init__.py
|
||||
===================
|
||||
|
||||
The ``__init__.py`` file should be placed inside the ``mopidy_soundspot``
|
||||
Python package. The root of your Python package should have an ``__version__``
|
||||
attribute with a :pep:`386` compliant version number, for example "0.1". Next,
|
||||
it should have a class named ``Extension`` which inherits from Mopidy's
|
||||
extension base class. This is the class referred to in the ``entry_points``
|
||||
part of ``setup.py``. Any imports of other files in your extension should be
|
||||
kept inside methods. This ensures that this file can be imported without
|
||||
raising :exc:`ImportError` exceptions for missing dependencies, etc.
|
||||
|
||||
::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
from mopidy.exceptions import ExtensionError
|
||||
from mopidy.utils import ext
|
||||
|
||||
|
||||
__version__ = '0.1'
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
name = 'Mopidy-Soundspot'
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
config_file = os.path.join(
|
||||
os.path.dirname(__file__), 'config.ini')
|
||||
return open(config_file).read()
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls, config):
|
||||
# ``config`` is the complete config document for the Mopidy
|
||||
# instance. The extension is free to check any config value it is
|
||||
# interested in, not just its own config values.
|
||||
|
||||
if not config.getboolean('soundspot', 'enabled'):
|
||||
return
|
||||
if not config.get('soundspot', 'username'):
|
||||
raise ExtensionError('Config soundspot.username not set')
|
||||
if not config.get('soundspot', 'password'):
|
||||
raise ExtensionError('Config soundspot.password not set')
|
||||
|
||||
@classmethod
|
||||
def validate_environment(cls):
|
||||
# This method can validate anything it wants about the environment
|
||||
# the extension is running in. Examples include checking if all
|
||||
# dependencies are installed.
|
||||
|
||||
try:
|
||||
import pysoundspot
|
||||
except ImportError as e:
|
||||
raise ExtensionError('pysoundspot library not found', e)
|
||||
|
||||
# You will typically only implement one of the next three methods
|
||||
# in a single extension.
|
||||
|
||||
@classmethod
|
||||
def get_frontend_class(cls):
|
||||
from .frontend import SoundspotFrontend
|
||||
return SoundspotFrontend
|
||||
|
||||
@classmethod
|
||||
def get_backend_class(cls):
|
||||
from .backend import SoundspotBackend
|
||||
return SoundspotBackend
|
||||
|
||||
@classmethod
|
||||
def register_gstreamer_elements(cls):
|
||||
from .mixer import SoundspotMixer
|
||||
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||
|
||||
|
||||
|
||||
Example config.ini
|
||||
==================
|
||||
|
||||
The default configuration for the extension is located in a ``config.ini`` file
|
||||
inside the Python package. It contains a single config section, with a name
|
||||
matching the short name used for the extension in the ``entry_points`` part of
|
||||
``setup.py``.
|
||||
|
||||
All extensions should include an ``enabled`` config which should default to
|
||||
``true``. Leave any configurations that doesn't have meaningful defaults blank,
|
||||
like ``username`` and ``password``.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[soundspot]
|
||||
enabled = true
|
||||
username =
|
||||
password =
|
||||
|
||||
|
||||
Example frontend
|
||||
================
|
||||
|
||||
If you want to *use* Mopidy's core API from your extension, then you want to
|
||||
implement a frontend.
|
||||
|
||||
The skeleton of a frontend would look like this. Notice that the frontend gets
|
||||
passed a reference to the core API when it's created. See the
|
||||
:ref:`frontend-api` for more details.
|
||||
|
||||
::
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.core import CoreListener
|
||||
|
||||
|
||||
class SoundspotFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
super(SoundspotFrontend, self).__init__()
|
||||
self.core = core
|
||||
|
||||
# Your frontend implementation
|
||||
|
||||
|
||||
Example backend
|
||||
===============
|
||||
|
||||
If you want to extend Mopidy to support new music and playlist sources, you
|
||||
want to implement a backend. A backend does not have access to Mopidy's core
|
||||
API at all and got a bunch of interfaces to implement.
|
||||
|
||||
The skeleton of a backend would look like this. See :ref:`backend-api` for more
|
||||
details.
|
||||
|
||||
::
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
|
||||
|
||||
class SoundspotBackend(pykka.ThreadingActor, base.BaseBackend):
|
||||
def __init__(self, audio):
|
||||
super(SoundspotBackend, self).__init__()
|
||||
self.audio = audio
|
||||
|
||||
# Your backend implementation
|
||||
|
||||
|
||||
Example GStreamer element
|
||||
=========================
|
||||
|
||||
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:`Extension.register_gstreamer_elements` method register all your
|
||||
custom GStreamer elements.
|
||||
|
||||
For examples of custom GStreamer elements implemented in Python, see
|
||||
:mod:`mopidy.audio.mixers`.
|
||||
|
||||
|
||||
Implementation steps
|
||||
====================
|
||||
|
||||
A rough plan of how to make the above document the reality of how Mopidy
|
||||
extensions work.
|
||||
|
||||
1. Implement :class:`mopidy.utils.ext.Extension` base class and the
|
||||
:exc:`mopidy.exceptions.ExtensionError` exception class.
|
||||
|
||||
2. Switch from using distutils to setuptools to package and install Mopidy so
|
||||
that we can register entry points for the bundled extensions and get
|
||||
information about all extensions available on the system from
|
||||
:mod:`pkg_resources`.
|
||||
|
||||
3. Add :class:`Extension` classes for all existing frontends and backends. Make
|
||||
sure to add default config files and config validation, even though this
|
||||
will not be used at this implementation stage.
|
||||
|
||||
4. Add entry points for the existing extensions in the ``setup.py`` file.
|
||||
|
||||
5. Rewrite the startup procedure to find extensions and thus frontends and
|
||||
backends via :mod:`pkg_resouces` instead of the ``FRONTENDS`` and
|
||||
``BACKENDS`` settings.
|
||||
|
||||
6. Remove the ``FRONTENDS`` and ``BACKENDS`` settings.
|
||||
|
||||
7. Switch to ini file based configuration, using :mod:`ConfigParser`. The
|
||||
default config is the combination of a core config file plus the config from
|
||||
each installed extension. To find the effective config for the system, the
|
||||
following config sources are added together, with the later ones overriding
|
||||
the earlier ones:
|
||||
|
||||
- the default config built from Mopidy core and all installed extensions,
|
||||
|
||||
- ``/etc/mopidy.conf``,
|
||||
|
||||
- ``~/.config/mopidy.conf``,
|
||||
|
||||
- any config file provided via command line arguments, and
|
||||
|
||||
- any config values provided via command line arguments.
|
||||
|
||||
8. Add command line options for:
|
||||
|
||||
- loading an additional config file for this execution of Mopidy,
|
||||
|
||||
- setting a config value for this execution of Mopidy,
|
||||
|
||||
- printing the effective config and exit, and
|
||||
|
||||
- write a config value permanently to ``~/.config/mopidy.conf`` and exit.
|
||||
@ -17,9 +17,10 @@ including Windows, Mac OS X, Linux, Android, and iOS.
|
||||
To install Mopidy, start by reading :ref:`installation`.
|
||||
|
||||
If you get stuck, we usually hang around at ``#mopidy`` at `irc.freenode.net
|
||||
<http://freenode.net/>`_. If you stumble into a bug or got a feature request,
|
||||
please create an issue in the `issue tracker
|
||||
<https://github.com/mopidy/mopidy/issues>`_.
|
||||
<http://freenode.net/>`_ and also got a `mailing list at Google Groups
|
||||
<https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_. If you stumble
|
||||
into a bug or got a feature request, please create an issue in the `issue
|
||||
tracker <https://github.com/mopidy/mopidy/issues>`_.
|
||||
|
||||
|
||||
Project resources
|
||||
@ -30,13 +31,14 @@ Project resources
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
|
||||
|
||||
User documentation
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
:maxdepth: 2
|
||||
|
||||
installation/index
|
||||
installation/raspberrypi
|
||||
@ -52,7 +54,7 @@ Reference documentation
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
:maxdepth: 2
|
||||
|
||||
api/index
|
||||
modules/index
|
||||
@ -62,9 +64,10 @@ Development documentation
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
:maxdepth: 2
|
||||
|
||||
development
|
||||
extensiondev
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
@ -209,8 +209,12 @@ software packages, as Wheezy is going to be the next release of Debian.
|
||||
|
||||
aplay /usr/share/sounds/alsa/Front_Center.wav
|
||||
|
||||
To make the change to analog output stick, you can add the ``amixer`` command
|
||||
to e.g. ``/etc/rc.local``, which will be executed when the system is
|
||||
If you hear a voice saying "Front Center", then your sound is working. Don't
|
||||
be concerned if this test sound includes static. Test your sound with
|
||||
GStreamer to determine the sound quality of Mopidy.
|
||||
|
||||
To make the change to analog output stick, you can add the ``amixer``
|
||||
command to e.g. ``/etc/rc.local``, which will be executed when the system is
|
||||
booting.
|
||||
|
||||
|
||||
|
||||
79
js/Gruntfile.js
Normal file
79
js/Gruntfile.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*global module:false*/
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
meta: {
|
||||
banner: "/*! Mopidy.js - built " +
|
||||
"<%= grunt.template.today('yyyy-mm-dd') %>\n" +
|
||||
" * http://www.mopidy.com/\n" +
|
||||
" * Copyright (c) <%= grunt.template.today('yyyy') %> " +
|
||||
"Stein Magnus Jodal and contributors\n" +
|
||||
" * 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"
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
options: {
|
||||
banner: "<%= meta.banner %>",
|
||||
stripBanners: true
|
||||
},
|
||||
all: {
|
||||
files: {
|
||||
"<%= meta.files.concat %>": [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js",
|
||||
"src/mopidy.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
indent: 4,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
quotmark: "double",
|
||||
undef: true,
|
||||
unused: true,
|
||||
eqnull: true,
|
||||
browser: true,
|
||||
devel: true,
|
||||
globals: {}
|
||||
},
|
||||
files: "<%= meta.files.own %>"
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
banner: "<%= meta.banner %>"
|
||||
},
|
||||
all: {
|
||||
files: {
|
||||
"<%= meta.files.minified %>": ["<%= meta.files.concat %>"]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files: "<%= meta.files.own %>",
|
||||
tasks: ["default"]
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask("test", ["jshint", "buster"]);
|
||||
grunt.registerTask("build", ["test", "concat", "uglify"]);
|
||||
grunt.registerTask("default", ["build"]);
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
grunt.loadNpmTasks("grunt-contrib-concat");
|
||||
grunt.loadNpmTasks("grunt-contrib-jshint");
|
||||
grunt.loadNpmTasks("grunt-contrib-uglify");
|
||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
||||
};
|
||||
82
js/README.md
Normal file
82
js/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
Mopidy.js
|
||||
=========
|
||||
|
||||
Mopidy.js is a JavaScript library that is installed as a part of Mopidy's HTTP
|
||||
frontend or from npm. The library makes Mopidy's core API available from the
|
||||
browser or a Node.js environment, using JSON-RPC messages over a WebSocket to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting it for browser use
|
||||
--------------------------
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
In the source repo, you can find the files at:
|
||||
|
||||
- `mopidy/frontends/http/data/mopidy.js`
|
||||
- `mopidy/frontends/http/data/mopidy.min.js`
|
||||
|
||||
|
||||
Getting it for Node.js use
|
||||
--------------------------
|
||||
|
||||
If you want to use Mopidy.js from Node.js instead of a browser, you can install
|
||||
Mopidy.js using npm:
|
||||
|
||||
npm install mopidy
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
|
||||
|
||||
Using the library
|
||||
-----------------
|
||||
|
||||
See Mopidy's [HTTP frontend
|
||||
documentation](http://docs.mopidy.com/en/latest/modules/frontends/http/).
|
||||
|
||||
|
||||
Building from source
|
||||
--------------------
|
||||
|
||||
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're
|
||||
running Ubuntu:
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
|
||||
|
||||
cd js/
|
||||
npm install
|
||||
|
||||
That's it.
|
||||
|
||||
You can now run the tests:
|
||||
|
||||
npm test
|
||||
|
||||
To run tests automatically when you save a file:
|
||||
|
||||
npm run-script watch
|
||||
|
||||
To run tests, concatenate, minify the source, and update the JavaScript files
|
||||
in `mopidy/frontends/http/data/`:
|
||||
|
||||
npm run-script build
|
||||
|
||||
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
|
||||
@ -1,62 +0,0 @@
|
||||
*********
|
||||
Mopidy.js
|
||||
*********
|
||||
|
||||
This is the source for the JavaScript library that is installed as a part of
|
||||
Mopidy's HTTP frontend. The library makes Mopidy's core API available from the
|
||||
browser, using JSON-RPC messages over a WebSocket to communicate with Mopidy.
|
||||
|
||||
|
||||
Getting it
|
||||
==========
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
available at:
|
||||
|
||||
- http://localhost:6680/mopidy/mopidy.js
|
||||
- http://localhost:6680/mopidy/mopidy.min.js
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
In the source repo, you can find the files at:
|
||||
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Building from source
|
||||
====================
|
||||
|
||||
1. Install `Node.js <http://nodejs.org/>`_ and npm. There is a PPA if you're
|
||||
running Ubuntu::
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
2. Enter the ``js/`` dir and install development dependencies::
|
||||
|
||||
cd js/
|
||||
npm install
|
||||
|
||||
That's it.
|
||||
|
||||
You can now run the tests::
|
||||
|
||||
npm test
|
||||
|
||||
To run tests automatically when you save a file::
|
||||
|
||||
npm run-script watch
|
||||
|
||||
To run tests, concatenate, minify the source, and update the JavaScript files
|
||||
in ``mopidy/frontends/http/data/``::
|
||||
|
||||
npm run-script build
|
||||
|
||||
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
|
||||
20
js/buster.js
20
js/buster.js
@ -1,8 +1,24 @@
|
||||
var config = module.exports;
|
||||
|
||||
config["tests"] = {
|
||||
config.browser_tests = {
|
||||
environment: "browser",
|
||||
libs: ["lib/**/*.js"],
|
||||
libs: [
|
||||
"lib/bane-*.js",
|
||||
"lib/when-define-shim.js",
|
||||
"lib/when-*.js"
|
||||
],
|
||||
sources: ["src/**/*.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"]
|
||||
|
||||
72
js/grunt.js
72
js/grunt.js
@ -1,72 +0,0 @@
|
||||
/*global module:false*/
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
meta: {
|
||||
banner: "/*! Mopidy.js - built " +
|
||||
"<%= grunt.template.today('yyyy-mm-dd') %>\n" +
|
||||
" * http://www.mopidy.com/\n" +
|
||||
" * Copyright (c) <%= grunt.template.today('yyyy') %> " +
|
||||
"Stein Magnus Jodal and contributors\n" +
|
||||
" * Licensed under the Apache License, Version 2.0 */"
|
||||
},
|
||||
dirs: {
|
||||
dest: "../mopidy/frontends/http/data"
|
||||
},
|
||||
lint: {
|
||||
files: ["grunt.js", "src/**/*.js", "test/**/*-test.js"]
|
||||
},
|
||||
buster: {
|
||||
test: {
|
||||
config: "buster.js"
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
dist: {
|
||||
src: [
|
||||
"<banner:meta.banner>",
|
||||
"lib/bane-*.js",
|
||||
"lib/when-*.js",
|
||||
"src/mopidy.js"
|
||||
],
|
||||
dest: "<%= dirs.dest %>/mopidy.js"
|
||||
}
|
||||
},
|
||||
min: {
|
||||
dist: {
|
||||
src: ["<banner:meta.banner>", "<config:concat.dist.dest>"],
|
||||
dest: "<%= dirs.dest %>/mopidy.min.js"
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files: "<config:lint.files>",
|
||||
tasks: "lint buster concat min"
|
||||
},
|
||||
jshint: {
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
indent: 4,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
quotmark: "double",
|
||||
undef: true,
|
||||
unused: true,
|
||||
eqnull: true,
|
||||
browser: true,
|
||||
devel: true
|
||||
},
|
||||
globals: {}
|
||||
},
|
||||
uglify: {}
|
||||
});
|
||||
|
||||
grunt.registerTask("test", "lint buster");
|
||||
grunt.registerTask("build", "lint buster concat min");
|
||||
grunt.registerTask("default", "build");
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
};
|
||||
@ -1,731 +0,0 @@
|
||||
/** @license MIT License (c) copyright B Cavalier & J Hann */
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @version 1.6.1
|
||||
*/
|
||||
|
||||
(function(define) { 'use strict';
|
||||
define(['module'], function () {
|
||||
var reduceArray, slice, undef;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
when.defer = defer; // Create a deferred
|
||||
when.resolve = resolve; // Create a resolved promise
|
||||
when.reject = reject; // Create a rejected promise
|
||||
|
||||
when.join = join; // Join 2 or more promises
|
||||
|
||||
when.all = all; // Resolve a list of promises
|
||||
when.some = some; // Resolve a sub-set of promises
|
||||
when.any = any; // Resolve one promise in a list
|
||||
|
||||
when.map = map; // Array.map() for promises
|
||||
when.reduce = reduce; // Array.reduce() for promises
|
||||
|
||||
when.chain = chain; // Make a promise trigger another resolver
|
||||
|
||||
when.isPromise = isPromise; // Determine if a thing is a promise
|
||||
|
||||
/**
|
||||
* Register an observer for a promise or immediate value.
|
||||
* @function
|
||||
* @name when
|
||||
* @namespace
|
||||
*
|
||||
* @param promiseOrValue {*}
|
||||
* @param {Function} [callback] callback to be called when promiseOrValue is
|
||||
* successfully fulfilled. If promiseOrValue is an immediate value, callback
|
||||
* will be invoked immediately.
|
||||
* @param {Function} [errback] callback to be called when promiseOrValue is
|
||||
* rejected.
|
||||
* @param {Function} [progressHandler] 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, callback, errback, progressHandler) {
|
||||
// Get a trusted promise for the input promiseOrValue, and then
|
||||
// register promise handlers
|
||||
return resolve(promiseOrValue).then(callback, errback, progressHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
|
||||
* promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise}
|
||||
* whose value is promiseOrValue if promiseOrValue is an immediate value.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue {*}
|
||||
* @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
|
||||
* returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
|
||||
* whose resolution value is:
|
||||
* * the resolution value of promiseOrValue if it's a foreign promise, or
|
||||
* * promiseOrValue if it's a value
|
||||
*/
|
||||
function resolve(promiseOrValue) {
|
||||
var promise, deferred;
|
||||
|
||||
if(promiseOrValue instanceof Promise) {
|
||||
// It's a when.js promise, so we trust it
|
||||
promise = promiseOrValue;
|
||||
|
||||
} else {
|
||||
// It's not a when.js promise. See if it's a foreign promise or a value.
|
||||
|
||||
// Some promises, particularly Q promises, provide a valueOf method that
|
||||
// attempts to synchronously return the fulfilled value of the promise, or
|
||||
// returns the unresolved promise itself. Attempting to break a fulfillment
|
||||
// value out of a promise appears to be necessary to break cycles between
|
||||
// Q and When attempting to coerce each-other's promises in an infinite loop.
|
||||
// For promises that do not implement "valueOf", the Object#valueOf is harmless.
|
||||
// See: https://github.com/kriskowal/q/issues/106
|
||||
// IMPORTANT: Must check for a promise here, since valueOf breaks other things
|
||||
// like Date.
|
||||
if (isPromise(promiseOrValue) && typeof promiseOrValue.valueOf === 'function') {
|
||||
promiseOrValue = promiseOrValue.valueOf();
|
||||
}
|
||||
|
||||
if(isPromise(promiseOrValue)) {
|
||||
// It looks like a thenable, but we don't know where it came from,
|
||||
// so we don't trust its implementation entirely. Introduce a trusted
|
||||
// middleman when.js promise
|
||||
deferred = defer();
|
||||
|
||||
// IMPORTANT: This is the only place when.js should ever call .then() on
|
||||
// an untrusted promise.
|
||||
promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
|
||||
promise = deferred.promise;
|
||||
|
||||
} else {
|
||||
// It's a value, not a promise. Create a resolved promise for it.
|
||||
promise = fulfilled(promiseOrValue);
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rejected promise for the supplied promiseOrValue. If
|
||||
* promiseOrValue is a value, it will be the rejection value of the
|
||||
* returned promise. If promiseOrValue is a promise, its
|
||||
* completion value will be the rejected value of the returned promise
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue {*} the rejected value of the returned {@link Promise}
|
||||
* @return {Promise} rejected {@link Promise}
|
||||
*/
|
||||
function reject(promiseOrValue) {
|
||||
return when(promiseOrValue, function(value) {
|
||||
return rejected(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted Promise constructor. A Promise created from this constructor is
|
||||
* a trusted when.js promise. Any other duck-typed promise is considered
|
||||
* untrusted.
|
||||
* @constructor
|
||||
* @name Promise
|
||||
*/
|
||||
function Promise(then) {
|
||||
this.then = then;
|
||||
}
|
||||
|
||||
Promise.prototype = {
|
||||
/**
|
||||
* Register a callback that will be called when a promise is
|
||||
* resolved or rejected. Optionally also register a progress handler.
|
||||
* Shortcut for .then(alwaysback, alwaysback, progback)
|
||||
* @memberOf Promise
|
||||
* @param alwaysback {Function}
|
||||
* @param progback {Function}
|
||||
* @return {Promise}
|
||||
*/
|
||||
always: function(alwaysback, progback) {
|
||||
return this.then(alwaysback, alwaysback, progback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a rejection handler. Shortcut for .then(null, errback)
|
||||
* @memberOf Promise
|
||||
* @param errback {Function}
|
||||
* @return {Promise}
|
||||
*/
|
||||
otherwise: function(errback) {
|
||||
return this.then(undef, errback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an already-resolved promise for the supplied value
|
||||
* @private
|
||||
*
|
||||
* @param value anything
|
||||
* @return {Promise}
|
||||
*/
|
||||
function fulfilled(value) {
|
||||
var p = new Promise(function(callback) {
|
||||
try {
|
||||
return resolve(callback ? callback(value) : value);
|
||||
} catch(e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an already-rejected {@link Promise} with the supplied
|
||||
* rejection reason.
|
||||
* @private
|
||||
*
|
||||
* @param reason rejection reason
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rejected(reason) {
|
||||
var p = new Promise(function(callback, errback) {
|
||||
try {
|
||||
return errback ? resolve(errback(reason)) : rejected(reason);
|
||||
} catch(e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new, Deferred with fully isolated resolver and promise parts,
|
||||
* either or both of which may be given out safely to consumers.
|
||||
* The Deferred itself has the full API: resolve, reject, progress, and
|
||||
* then. The resolver has resolve, reject, and progress. The promise
|
||||
* only has then.
|
||||
* @memberOf when
|
||||
* @function
|
||||
*
|
||||
* @return {Deferred}
|
||||
*/
|
||||
function defer() {
|
||||
var deferred, promise, handlers, progressHandlers,
|
||||
_then, _progress, _resolve;
|
||||
|
||||
/**
|
||||
* The promise for the new deferred
|
||||
* @type {Promise}
|
||||
*/
|
||||
promise = new Promise(then);
|
||||
|
||||
/**
|
||||
* The full Deferred object, with {@link Promise} and {@link Resolver} parts
|
||||
* @class Deferred
|
||||
* @name Deferred
|
||||
*/
|
||||
deferred = {
|
||||
then: then,
|
||||
resolve: promiseResolve,
|
||||
reject: promiseReject,
|
||||
// TODO: Consider renaming progress() to notify()
|
||||
progress: promiseProgress,
|
||||
|
||||
promise: promise,
|
||||
|
||||
resolver: {
|
||||
resolve: promiseResolve,
|
||||
reject: promiseReject,
|
||||
progress: promiseProgress
|
||||
}
|
||||
};
|
||||
|
||||
handlers = [];
|
||||
progressHandlers = [];
|
||||
|
||||
/**
|
||||
* Pre-resolution then() that adds the supplied callback, errback, and progback
|
||||
* functions to the registered listeners
|
||||
* @private
|
||||
*
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @throws {Error} if any argument is not null, undefined, or a Function
|
||||
*/
|
||||
_then = function(callback, errback, progback) {
|
||||
var deferred, progressHandler;
|
||||
|
||||
deferred = defer();
|
||||
progressHandler = progback
|
||||
? function(update) {
|
||||
try {
|
||||
// Allow progress handler to transform progress event
|
||||
deferred.progress(progback(update));
|
||||
} catch(e) {
|
||||
// Use caught value as progress
|
||||
deferred.progress(e);
|
||||
}
|
||||
}
|
||||
: deferred.progress;
|
||||
|
||||
handlers.push(function(promise) {
|
||||
promise.then(callback, errback)
|
||||
.then(deferred.resolve, deferred.reject, progressHandler);
|
||||
});
|
||||
|
||||
progressHandlers.push(progressHandler);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Issue a progress event, notifying all progress listeners
|
||||
* @private
|
||||
* @param update {*} progress event payload to pass to all listeners
|
||||
*/
|
||||
_progress = function(update) {
|
||||
processQueue(progressHandlers, update);
|
||||
return update;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transition from pre-resolution state to post-resolution state, notifying
|
||||
* all listeners of the resolution or rejection
|
||||
* @private
|
||||
* @param completed {Promise} the completed value of this deferred
|
||||
*/
|
||||
_resolve = function(completed) {
|
||||
completed = resolve(completed);
|
||||
|
||||
// Replace _then with one that directly notifies with the result.
|
||||
_then = completed.then;
|
||||
// Replace _resolve so that this Deferred can only be completed once
|
||||
_resolve = resolve;
|
||||
// Make _progress a noop, to disallow progress for the resolved promise.
|
||||
_progress = noop;
|
||||
|
||||
// Notify handlers
|
||||
processQueue(handlers, completed);
|
||||
|
||||
// Free progressHandlers array since we'll never issue progress events
|
||||
progressHandlers = handlers = undef;
|
||||
|
||||
return completed;
|
||||
};
|
||||
|
||||
return deferred;
|
||||
|
||||
/**
|
||||
* Wrapper to allow _then to be replaced safely
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @return {Promise} new Promise
|
||||
* @throws {Error} if any argument is not null, undefined, or a Function
|
||||
*/
|
||||
function then(callback, errback, progback) {
|
||||
return _then(callback, errback, progback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _resolve to be replaced
|
||||
*/
|
||||
function promiseResolve(val) {
|
||||
return _resolve(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _resolve to be replaced
|
||||
*/
|
||||
function promiseReject(err) {
|
||||
return _resolve(rejected(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to allow _progress to be replaced
|
||||
* @param {*} update progress update
|
||||
*/
|
||||
function promiseProgress(update) {
|
||||
return _progress(update);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if promiseOrValue is a promise or not. Uses the feature
|
||||
* test from http://wiki.commonjs.org/wiki/Promises/A to determine if
|
||||
* promiseOrValue is a promise.
|
||||
*
|
||||
* @param {*} promiseOrValue anything
|
||||
* @returns {Boolean} true if promiseOrValue is a {@link Promise}
|
||||
*/
|
||||
function isPromise(promiseOrValue) {
|
||||
return promiseOrValue && typeof promiseOrValue.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.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promisesOrValues {Array} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param howMany {Number} number of promisesOrValues to resolve
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @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, callback, errback, progback) {
|
||||
|
||||
checkCallbacks(2, arguments);
|
||||
|
||||
return when(promisesOrValues, function(promisesOrValues) {
|
||||
|
||||
var toResolve, toReject, values, reasons, deferred, fulfillOne, rejectOne, progress, len, i;
|
||||
|
||||
len = promisesOrValues.length >>> 0;
|
||||
|
||||
toResolve = Math.max(0, Math.min(howMany, len));
|
||||
values = [];
|
||||
|
||||
toReject = (len - toResolve) + 1;
|
||||
reasons = [];
|
||||
|
||||
deferred = defer();
|
||||
|
||||
// No items in the input, resolve immediately
|
||||
if (!toResolve) {
|
||||
deferred.resolve(values);
|
||||
|
||||
} else {
|
||||
progress = deferred.progress;
|
||||
|
||||
rejectOne = function(reason) {
|
||||
reasons.push(reason);
|
||||
if(!--toReject) {
|
||||
fulfillOne = rejectOne = noop;
|
||||
deferred.reject(reasons);
|
||||
}
|
||||
};
|
||||
|
||||
fulfillOne = function(val) {
|
||||
// This orders the values based on promise resolution order
|
||||
// Another strategy would be to use the original position of
|
||||
// the corresponding promise.
|
||||
values.push(val);
|
||||
|
||||
if (!--toResolve) {
|
||||
fulfillOne = rejectOne = noop;
|
||||
deferred.resolve(values);
|
||||
}
|
||||
};
|
||||
|
||||
for(i = 0; i < len; ++i) {
|
||||
if(i in promisesOrValues) {
|
||||
when(promisesOrValues[i], fulfiller, rejecter, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.then(callback, errback, progback);
|
||||
|
||||
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.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param [callback] {Function} resolution handler
|
||||
* @param [errback] {Function} rejection handler
|
||||
* @param [progback] {Function} progress handler
|
||||
* @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, callback, errback, progback) {
|
||||
|
||||
function unwrapSingleResult(val) {
|
||||
return callback ? callback(val[0]) : val[0];
|
||||
}
|
||||
|
||||
return some(promisesOrValues, 1, unwrapSingleResult, errback, progback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 promisesOrValues {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param [callback] {Function}
|
||||
* @param [errback] {Function}
|
||||
* @param [progressHandler] {Function}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function all(promisesOrValues, callback, errback, progressHandler) {
|
||||
checkCallbacks(1, arguments);
|
||||
return map(promisesOrValues, identity).then(callback, errback, progressHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple promises into a single returned promise.
|
||||
* @memberOf when
|
||||
* @param {Promise|*} [...promises] two or more promises to join
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional map function, similar to `Array.prototype.map()`, but allows
|
||||
* input to contain {@link Promise}s and/or values, and mapFunc may return
|
||||
* either a value or a {@link Promise}
|
||||
*
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promise {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param mapFunc {Function} mapping function mapFunc(value) which may return
|
||||
* either a {@link Promise} or value
|
||||
* @returns {Promise} a {@link Promise} that will resolve to an array containing
|
||||
* the mapped output values.
|
||||
*/
|
||||
function map(promise, mapFunc) {
|
||||
return when(promise, function(array) {
|
||||
var results, len, toResolve, resolve, reject, i, d;
|
||||
|
||||
// Since we know the resulting length, we can preallocate the results
|
||||
// array to avoid array expansions.
|
||||
toResolve = len = array.length >>> 0;
|
||||
results = [];
|
||||
d = defer();
|
||||
|
||||
if(!toResolve) {
|
||||
d.resolve(results);
|
||||
} else {
|
||||
|
||||
reject = d.reject;
|
||||
resolve = function resolveOne(item, i) {
|
||||
when(item, mapFunc).then(function(mapped) {
|
||||
results[i] = mapped;
|
||||
|
||||
if(!--toResolve) {
|
||||
d.resolve(results);
|
||||
}
|
||||
}, reject);
|
||||
};
|
||||
|
||||
// Since mapFunc may be async, get all invocations of it into flight
|
||||
for(i = 0; i < len; i++) {
|
||||
if(i in array) {
|
||||
resolve(array[i], i);
|
||||
} else {
|
||||
--toResolve;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return d.promise;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
|
||||
* input may contain {@link Promise}s and/or values, and reduceFunc
|
||||
* may return either a value or a {@link Promise}, *and* initialValue may
|
||||
* be a {@link Promise} for the starting value.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promise {Array|Promise} array of anything, may contain a mix
|
||||
* of {@link Promise}s and values. May also be a {@link Promise} for
|
||||
* an array.
|
||||
* @param reduceFunc {Function} 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.
|
||||
* @param [initialValue] {*} starting value, or a {@link Promise} for the starting value
|
||||
* @returns {Promise} that will resolve to the final reduced value
|
||||
*/
|
||||
function reduce(promise, reduceFunc /*, initialValue */) {
|
||||
var args = slice.call(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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that resolution of promiseOrValue will complete resolver with the completion
|
||||
* value of promiseOrValue, or instead with resolveValue if it is provided.
|
||||
* @memberOf when
|
||||
*
|
||||
* @param promiseOrValue
|
||||
* @param resolver {Resolver}
|
||||
* @param [resolveValue] anything
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function chain(promiseOrValue, resolver, resolveValue) {
|
||||
var useResolveValue = arguments.length > 2;
|
||||
|
||||
return when(promiseOrValue,
|
||||
function(val) {
|
||||
return resolver.resolve(useResolveValue ? resolveValue : val);
|
||||
},
|
||||
resolver.reject,
|
||||
resolver.progress
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Utility functions
|
||||
//
|
||||
|
||||
function processQueue(queue, value) {
|
||||
var handler, i = 0;
|
||||
|
||||
while (handler = queue[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that checks arrayOfCallbacks to ensure that each element is either
|
||||
* a function, or null or undefined.
|
||||
* @private
|
||||
*
|
||||
* @param arrayOfCallbacks {Array} array to check
|
||||
* @throws {Error} if any element of arrayOfCallbacks is something other than
|
||||
* a Functions, null, or undefined.
|
||||
*/
|
||||
function checkCallbacks(start, arrayOfCallbacks) {
|
||||
var arg, i = arrayOfCallbacks.length;
|
||||
|
||||
while(i > start) {
|
||||
arg = arrayOfCallbacks[--i];
|
||||
|
||||
if (arg != null && typeof arg != 'function') {
|
||||
throw new Error('arg '+i+' must be a function');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No-Op function used in method replacement
|
||||
* @private
|
||||
*/
|
||||
function noop() {}
|
||||
|
||||
slice = [].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.
|
||||
reduceArray = [].reduce ||
|
||||
function(reduceFunc /*, initialValue */) {
|
||||
/*jshint maxcomplexity: 7*/
|
||||
|
||||
// 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
|
||||
|
||||
var arr, args, reduced, len, i;
|
||||
|
||||
i = 0;
|
||||
// This generates a jshint warning, despite being valid
|
||||
// "Missing 'new' prefix when invoking a constructor."
|
||||
// See https://github.com/jshint/jshint/issues/392
|
||||
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) {
|
||||
// Skip holes
|
||||
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 (deps, factory) { typeof exports === 'object'
|
||||
? (module.exports = factory())
|
||||
: (this.when = factory());
|
||||
}
|
||||
// Boilerplate for AMD, Node, and browser global
|
||||
);
|
||||
787
js/lib/when-2.0.0.js
Normal file
787
js/lib/when-2.0.0.js
Normal file
@ -0,0 +1,787 @@
|
||||
/** @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.0.0
|
||||
*/
|
||||
(function(define) { 'use strict';
|
||||
define(function () {
|
||||
|
||||
// Public API
|
||||
|
||||
when.defer = defer; // Create a deferred
|
||||
when.resolve = resolve; // Create a resolved promise
|
||||
when.reject = reject; // Create a rejected promise
|
||||
|
||||
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.any = any; // One-winner race
|
||||
when.some = some; // Multi-winner race
|
||||
|
||||
when.isPromise = isPromise; // Determine if a thing is a promise
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @name Promise
|
||||
*/
|
||||
function Promise(then) {
|
||||
this.then = then;
|
||||
}
|
||||
|
||||
Promise.prototype = {
|
||||
/**
|
||||
* 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) {
|
||||
var self = this;
|
||||
|
||||
return this.then(injectHandler, injectHandler).yield(self);
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 new Deferred with fully isolated resolver and promise parts,
|
||||
* either or both of which may be given out safely to consumers.
|
||||
* The resolver has resolve, reject, and progress. The promise
|
||||
* only has then.
|
||||
*
|
||||
* @return {{
|
||||
* promise: 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.
|
||||
* @private (for now)
|
||||
* @param {function} resolver function(resolve, reject, notify)
|
||||
* @returns {Promise} promise whose fate is determine by resolver
|
||||
*/
|
||||
function promise(resolver) {
|
||||
var value, handlers = [];
|
||||
|
||||
// Call the provider resolver to seal the promise's fate
|
||||
try {
|
||||
resolver(promiseResolve, promiseReject, promiseNotify);
|
||||
} catch(e) {
|
||||
promiseReject(e);
|
||||
}
|
||||
|
||||
// Return the promise
|
||||
return new Promise(then);
|
||||
|
||||
/**
|
||||
* Register handlers for this promise.
|
||||
* @param [onFulfilled] {Function} fulfillment handler
|
||||
* @param [onRejected] {Function} rejection handler
|
||||
* @param [onProgress] {Function} progress handler
|
||||
* @return {Promise} new Promise
|
||||
*/
|
||||
function then(onFulfilled, onRejected, onProgress) {
|
||||
return promise(function(resolve, reject, notify) {
|
||||
handlers
|
||||
// Call handlers later, after resolution
|
||||
? handlers.push(function(value) {
|
||||
value.then(onFulfilled, onRejected, onProgress)
|
||||
.then(resolve, reject, notify);
|
||||
})
|
||||
// Call handlers soon, but not in the current stack
|
||||
: enqueue(function() {
|
||||
value.then(onFulfilled, onRejected, onProgress)
|
||||
.then(resolve, reject, notify);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(!handlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = coerce(val);
|
||||
scheduleHandlers(handlers, value);
|
||||
|
||||
handlers = undef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(handlers) {
|
||||
scheduleHandlers(handlers, progressing(update));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces x to a trusted Promise
|
||||
*
|
||||
* @private
|
||||
* @param {*} x thing to coerce
|
||||
* @returns {Promise} 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;
|
||||
} else if (x !== Object(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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an already-fulfilled promise for the supplied value
|
||||
* @private
|
||||
* @param {*} value
|
||||
* @return {Promise} fulfilled promise
|
||||
*/
|
||||
function fulfilled(value) {
|
||||
var self = new Promise(function (onFulfilled) {
|
||||
try {
|
||||
return typeof onFulfilled == 'function'
|
||||
? coerce(onFulfilled(value)) : self;
|
||||
} catch (e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an already-rejected promise with the supplied rejection reason.
|
||||
* @private
|
||||
* @param {*} reason
|
||||
* @return {Promise} rejected promise
|
||||
*/
|
||||
function rejected(reason) {
|
||||
var self = new Promise(function (_, onRejected) {
|
||||
try {
|
||||
return typeof onRejected == 'function'
|
||||
? coerce(onRejected(reason)) : self;
|
||||
} catch (e) {
|
||||
return rejected(e);
|
||||
}
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress promise with the supplied update.
|
||||
* @private
|
||||
* @param {*} update
|
||||
* @return {Promise} progress promise
|
||||
*/
|
||||
function progressing(update) {
|
||||
var self = new Promise(function (_, __, onProgress) {
|
||||
try {
|
||||
return typeof onProgress == 'function'
|
||||
? progressing(onProgress(update)) : self;
|
||||
} catch (e) {
|
||||
return progressing(e);
|
||||
}
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 scheduleHandlers(handlers, value) {
|
||||
enqueue(function() {
|
||||
var handler, i = 0;
|
||||
while (handler = handlers[i++]) {
|
||||
handler(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if promiseOrValue is a promise or not
|
||||
*
|
||||
* @param {*} promiseOrValue anything
|
||||
* @returns {boolean} true if promiseOrValue is a {@link Promise}
|
||||
*/
|
||||
function isPromise(promiseOrValue) {
|
||||
return promiseOrValue && typeof promiseOrValue.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] resolution handler
|
||||
* @param {function?} [onRejected] rejection handler
|
||||
* @param {function?} [onProgress] progress handler
|
||||
* @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) {
|
||||
|
||||
checkCallbacks(2, arguments);
|
||||
|
||||
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 = noop;
|
||||
reject(reasons);
|
||||
}
|
||||
};
|
||||
|
||||
fulfillOne = function(val) {
|
||||
// This orders the values based on promise resolution order
|
||||
values.push(val);
|
||||
if (!--toResolve) {
|
||||
fulfillOne = rejectOne = noop;
|
||||
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] resolution handler
|
||||
* @param {function?} [onRejected] rejection handler
|
||||
* @param {function?} [onProgress] progress handler
|
||||
* @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] resolution handler
|
||||
* @param {function?} [onRejected] rejection handler
|
||||
* @param {function?} [onProgress] progress handler
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function all(promisesOrValues, onFulfilled, onRejected, onProgress) {
|
||||
checkCallbacks(1, arguments);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional map function, similar to `Array.prototype.map()`, but allows
|
||||
* input to contain {@link Promise}s and/or values, and mapFunc may return
|
||||
* either a value or a {@link Promise}
|
||||
*
|
||||
* @param {Array|Promise} array array of anything, may contain a mix
|
||||
* of {@link Promise}s and values
|
||||
* @param {function} mapFunc mapping function mapFunc(value) which may return
|
||||
* either a {@link Promise} or value
|
||||
* @returns {Promise} a {@link Promise} that will resolve to an array containing
|
||||
* the mapped output values.
|
||||
*/
|
||||
function map(array, mapFunc) {
|
||||
return when(array, function(array) {
|
||||
|
||||
return promise(resolveMap);
|
||||
|
||||
function resolveMap(resolve, reject, notify) {
|
||||
var results, len, toResolve, resolveOne, 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);
|
||||
} else {
|
||||
|
||||
resolveOne = function(item, i) {
|
||||
when(item, mapFunc).then(function(mapped) {
|
||||
results[i] = mapped;
|
||||
|
||||
if(!--toResolve) {
|
||||
resolve(results);
|
||||
}
|
||||
}, reject, notify);
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Utilities, etc.
|
||||
//
|
||||
|
||||
var reduceArray, slice, fcall, nextTick, handlerQueue,
|
||||
timeout, funcProto, call, arrayProto, undef;
|
||||
|
||||
//
|
||||
// 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) {
|
||||
scheduleDrainQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the queue to be drained in the next tick.
|
||||
*/
|
||||
function scheduleDrainQueue() {
|
||||
nextTick(drainQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the handler queue entirely or partially, 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 function and array utils
|
||||
//
|
||||
/*global setImmediate:true*/
|
||||
|
||||
// capture setTimeout to avoid being caught by fake timers used in time based tests
|
||||
timeout = setTimeout;
|
||||
nextTick = typeof setImmediate === 'function'
|
||||
? typeof window === 'undefined'
|
||||
? setImmediate
|
||||
: setImmediate.bind(window)
|
||||
: typeof process === 'object'
|
||||
? process.nextTick
|
||||
: function(task) { timeout(task, 0); };
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
//
|
||||
// Utility functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Helper that checks arrayOfCallbacks to ensure that each element is either
|
||||
* a function, or null or undefined.
|
||||
* @private
|
||||
* @param {number} start index at which to start checking items in arrayOfCallbacks
|
||||
* @param {Array} arrayOfCallbacks array to check
|
||||
* @throws {Error} if any element of arrayOfCallbacks is something other than
|
||||
* a functions, null, or undefined.
|
||||
*/
|
||||
function checkCallbacks(start, arrayOfCallbacks) {
|
||||
// TODO: Promises/A+ update type checking and docs
|
||||
var arg, i = arrayOfCallbacks.length;
|
||||
|
||||
while(i > start) {
|
||||
arg = arrayOfCallbacks[--i];
|
||||
|
||||
if (arg != null && typeof arg != 'function') {
|
||||
throw new Error('arg '+i+' must be a function');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function identity(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
return when;
|
||||
});
|
||||
})(
|
||||
typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); }
|
||||
);
|
||||
11
js/lib/when-define-shim.js
Normal file
11
js/lib/when-define-shim.js
Normal file
@ -0,0 +1,11 @@
|
||||
if (typeof window !== "undefined") {
|
||||
window.define = function (factory) {
|
||||
try {
|
||||
delete window.define;
|
||||
} catch (e) {
|
||||
window.define = void 0; // IE
|
||||
}
|
||||
window.when = factory();
|
||||
};
|
||||
window.define.amd = {};
|
||||
}
|
||||
@ -1,15 +1,36 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"buster": "*",
|
||||
"grunt": "*",
|
||||
"grunt-buster": "*",
|
||||
"phantomjs": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"build": "grunt build",
|
||||
"watch": "grunt watch"
|
||||
}
|
||||
"name": "mopidy",
|
||||
"version": "0.1.0",
|
||||
"description": "Client lib for controlling a Mopidy music server over a WebSocket",
|
||||
"homepage": "http://www.mopidy.com/",
|
||||
"author": {
|
||||
"name": "Stein Magnus Jodal",
|
||||
"email": "stein.magnus@jodal.no",
|
||||
"url": "http://www.jodal.no"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/mopidy/mopidy.git"
|
||||
},
|
||||
"main": "src/mopidy.js",
|
||||
"dependencies": {
|
||||
"bane": "~0.4.0",
|
||||
"faye-websocket": "~0.4.4",
|
||||
"when": "~2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.6.12",
|
||||
"grunt": "~0.4.0",
|
||||
"grunt-buster": "~0.1.2",
|
||||
"grunt-contrib-concat": "~0.1.3",
|
||||
"grunt-contrib-jshint": "~0.2.0",
|
||||
"grunt-contrib-uglify": "~0.1.2",
|
||||
"grunt-contrib-watch": "~0.3.1",
|
||||
"phantomjs": "~1.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"build": "grunt build",
|
||||
"watch": "grunt watch"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
/*global bane:false, when:false*/
|
||||
/*global exports:false, require:false*/
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var bane = require("bane");
|
||||
var websocket = require("faye-websocket");
|
||||
var when = require("when");
|
||||
}
|
||||
|
||||
function Mopidy(settings) {
|
||||
if (!(this instanceof Mopidy)) {
|
||||
@ -20,9 +26,17 @@ function Mopidy(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
Mopidy.WebSocket = websocket.Client;
|
||||
} else {
|
||||
Mopidy.WebSocket = window.WebSocket;
|
||||
}
|
||||
|
||||
Mopidy.prototype._configure = function (settings) {
|
||||
var currentHost = (typeof document !== "undefined" &&
|
||||
document.location.host) || "localhost";
|
||||
settings.webSocketUrl = settings.webSocketUrl ||
|
||||
"ws://" + document.location.host + "/mopidy/ws/";
|
||||
"ws://" + currentHost + "/mopidy/ws/";
|
||||
|
||||
if (settings.autoConnect !== false) {
|
||||
settings.autoConnect = true;
|
||||
@ -35,7 +49,7 @@ Mopidy.prototype._configure = function (settings) {
|
||||
};
|
||||
|
||||
Mopidy.prototype._getConsole = function () {
|
||||
var console = window.console || {};
|
||||
var console = typeof console !== "undefined" && console || {};
|
||||
|
||||
console.log = console.log || function () {};
|
||||
console.warn = console.warn || function () {};
|
||||
@ -63,7 +77,7 @@ Mopidy.prototype._delegateEvents = function () {
|
||||
|
||||
Mopidy.prototype.connect = function () {
|
||||
if (this._webSocket) {
|
||||
if (this._webSocket.readyState === WebSocket.OPEN) {
|
||||
if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) {
|
||||
return;
|
||||
} else {
|
||||
this._webSocket.close();
|
||||
@ -71,7 +85,7 @@ Mopidy.prototype.connect = function () {
|
||||
}
|
||||
|
||||
this._webSocket = this._settings.webSocket ||
|
||||
new WebSocket(this._settings.webSocketUrl);
|
||||
new Mopidy.WebSocket(this._settings.webSocketUrl);
|
||||
|
||||
this._webSocket.onclose = function (close) {
|
||||
this.emit("websocket:close", close);
|
||||
@ -136,17 +150,17 @@ Mopidy.prototype._send = function (message) {
|
||||
var deferred = when.defer();
|
||||
|
||||
switch (this._webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
case Mopidy.WebSocket.CONNECTING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is still connecting"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
case Mopidy.WebSocket.CLOSING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closing"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
case Mopidy.WebSocket.CLOSED:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closed"
|
||||
});
|
||||
@ -228,7 +242,7 @@ Mopidy.prototype._handleEvent = function (eventMessage) {
|
||||
};
|
||||
|
||||
Mopidy.prototype._getApiSpec = function () {
|
||||
this._send({method: "core.describe"})
|
||||
return this._send({method: "core.describe"})
|
||||
.then(this._createApi.bind(this), this._handleWebSocketError)
|
||||
.then(null, this._handleWebSocketError);
|
||||
};
|
||||
@ -280,3 +294,7 @@ Mopidy.prototype._snakeToCamel = function (name) {
|
||||
return match.toUpperCase().replace("_", "");
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof exports === "object") {
|
||||
exports.Mopidy = Mopidy;
|
||||
}
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
/*global buster:false, assert:false, refute:false, when:false, Mopidy:false*/
|
||||
/*global require:false, assert:false, refute:false*/
|
||||
|
||||
if (typeof module === "object" && typeof require === "function") {
|
||||
var buster = require("buster");
|
||||
var Mopidy = require("../src/mopidy").Mopidy;
|
||||
var when = require("when");
|
||||
}
|
||||
|
||||
buster.testCase("Mopidy", {
|
||||
setUp: function () {
|
||||
@ -14,10 +20,11 @@ buster.testCase("Mopidy", {
|
||||
fakeWebSocket.OPEN = 1;
|
||||
fakeWebSocket.CLOSING = 2;
|
||||
fakeWebSocket.CLOSED = 3;
|
||||
this.realWebSocket = WebSocket;
|
||||
window.WebSocket = fakeWebSocket;
|
||||
|
||||
this.webSocketConstructorStub = this.stub(window, "WebSocket");
|
||||
this.realWebSocket = Mopidy.WebSocket;
|
||||
Mopidy.WebSocket = fakeWebSocket;
|
||||
|
||||
this.webSocketConstructorStub = this.stub(Mopidy, "WebSocket");
|
||||
|
||||
this.webSocket = {
|
||||
close: this.stub(),
|
||||
@ -27,15 +34,18 @@ buster.testCase("Mopidy", {
|
||||
},
|
||||
|
||||
tearDown: function () {
|
||||
window.WebSocket = this.realWebSocket;
|
||||
Mopidy.WebSocket = this.realWebSocket;
|
||||
},
|
||||
|
||||
"constructor": {
|
||||
"connects when autoConnect is true": function () {
|
||||
new Mopidy({autoConnect: true});
|
||||
|
||||
var currentHost = typeof document !== "undefined" &&
|
||||
document.location.host || "localhost";
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + document.location.host + "/mopidy/ws/");
|
||||
"ws://" + currentHost + "/mopidy/ws/");
|
||||
},
|
||||
|
||||
"does not connect when autoConnect is false": function () {
|
||||
@ -67,12 +77,15 @@ buster.testCase("Mopidy", {
|
||||
|
||||
mopidy.connect();
|
||||
|
||||
var currentHost = typeof document !== "undefined" &&
|
||||
document.location.host || "localhost";
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + document.location.host + "/mopidy/ws/");
|
||||
"ws://" + currentHost + "/mopidy/ws/");
|
||||
},
|
||||
|
||||
"does nothing when the WebSocket is open": function () {
|
||||
this.webSocket.readyState = WebSocket.OPEN;
|
||||
this.webSocket.readyState = Mopidy.WebSocket.OPEN;
|
||||
var mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
|
||||
mopidy.connect();
|
||||
@ -367,7 +380,7 @@ buster.testCase("Mopidy", {
|
||||
},
|
||||
|
||||
"immediately rejects request if CONNECTING": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CONNECTING;
|
||||
this.mopidy._webSocket.readyState = Mopidy.WebSocket.CONNECTING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
@ -381,7 +394,7 @@ buster.testCase("Mopidy", {
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSING": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSING;
|
||||
this.mopidy._webSocket.readyState = Mopidy.WebSocket.CLOSING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
@ -395,7 +408,7 @@ buster.testCase("Mopidy", {
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSED": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSED;
|
||||
this.mopidy._webSocket.readyState = Mopidy.WebSocket.CLOSED;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
@ -597,16 +610,16 @@ buster.testCase("Mopidy", {
|
||||
assert.calledOnceWith(stub);
|
||||
},
|
||||
|
||||
"gets Api description from server and calls _createApi": function () {
|
||||
"gets Api description from server and calls _createApi": function (done) {
|
||||
var methods = {};
|
||||
var sendStub = this.stub(this.mopidy, "_send");
|
||||
sendStub.returns(when.resolve(methods));
|
||||
var _createApiStub = this.stub(this.mopidy, "_createApi");
|
||||
|
||||
this.mopidy._getApiSpec();
|
||||
|
||||
assert.calledOnceWith(sendStub, {method: "core.describe"});
|
||||
assert.calledOnceWith(_createApiStub, methods);
|
||||
this.mopidy._getApiSpec().then(done(function () {
|
||||
assert.calledOnceWith(sendStub, {method: "core.describe"});
|
||||
assert.calledOnceWith(_createApiStub, methods);
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.12.0'
|
||||
__version__ = '0.13.0'
|
||||
|
||||
|
||||
from mopidy import settings as default_settings_module
|
||||
|
||||
@ -9,6 +9,8 @@ import sys
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
import pykka.debug
|
||||
|
||||
|
||||
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
|
||||
# processing by GStreamer. This needs to be done before GStreamer is imported,
|
||||
@ -43,15 +45,11 @@ logger = logging.getLogger('mopidy.main')
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, process.exit_handler)
|
||||
signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
|
||||
|
||||
loop = gobject.MainLoop()
|
||||
options = parse_options()
|
||||
|
||||
if options.debug_thread or settings.DEBUG_THREAD:
|
||||
debug_thread = process.DebugThread()
|
||||
debug_thread.start()
|
||||
signal.signal(signal.SIGUSR1, debug_thread.handler)
|
||||
|
||||
try:
|
||||
log.setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
check_old_folders()
|
||||
|
||||
@ -2,5 +2,8 @@ from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import Audio
|
||||
from .dummy import DummyAudio
|
||||
from .listener import AudioListener
|
||||
from .constants import PlaybackState
|
||||
from .utils import (calculate_duration, create_buffer, millisecond_to_clocktime,
|
||||
supported_uri_schemes)
|
||||
|
||||
@ -12,7 +12,7 @@ import pykka
|
||||
from mopidy import settings
|
||||
from mopidy.utils import process
|
||||
|
||||
from . import mixers
|
||||
from . import mixers, utils
|
||||
from .constants import PlaybackState
|
||||
from .listener import AudioListener
|
||||
|
||||
@ -21,6 +21,9 @@ logger = logging.getLogger('mopidy.audio')
|
||||
mixers.register_mixers()
|
||||
|
||||
|
||||
MB = 1 << 20
|
||||
|
||||
|
||||
class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
@ -39,16 +42,19 @@ class Audio(pykka.ThreadingActor):
|
||||
super(Audio, self).__init__()
|
||||
|
||||
self._playbin = None
|
||||
self._signal_ids = {} # {(element, event): signal_id}
|
||||
|
||||
self._mixer = None
|
||||
self._mixer_track = None
|
||||
self._mixer_scale = None
|
||||
self._software_mixing = False
|
||||
self._appsrc = None
|
||||
self._volume_set = None
|
||||
|
||||
self._notify_source_signal_id = None
|
||||
self._about_to_finish_id = None
|
||||
self._message_signal_id = None
|
||||
self._appsrc = None
|
||||
self._appsrc_caps = None
|
||||
self._appsrc_need_data_callback = None
|
||||
self._appsrc_enough_data_callback = None
|
||||
self._appsrc_seek_data_callback = None
|
||||
|
||||
def on_start(self):
|
||||
try:
|
||||
@ -65,42 +71,75 @@ class Audio(pykka.ThreadingActor):
|
||||
self._teardown_mixer()
|
||||
self._teardown_playbin()
|
||||
|
||||
def _connect(self, element, event, *args):
|
||||
"""Helper to keep track of signal ids based on element+event"""
|
||||
self._signal_ids[(element, event)] = element.connect(event, *args)
|
||||
|
||||
def _disconnect(self, element, event):
|
||||
"""Helper to disconnect signals created with _connect helper."""
|
||||
signal_id = self._signal_ids.pop((element, event), None)
|
||||
if signal_id is not None:
|
||||
element.disconnect(signal_id)
|
||||
|
||||
def _setup_playbin(self):
|
||||
self._playbin = gst.element_factory_make('playbin2')
|
||||
playbin = gst.element_factory_make('playbin2')
|
||||
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
self._playbin.set_property('video-sink', fakesink)
|
||||
playbin.set_property('video-sink', fakesink)
|
||||
|
||||
self._about_to_finish_id = self._playbin.connect(
|
||||
'about-to-finish', self._on_about_to_finish)
|
||||
self._notify_source_signal_id = self._playbin.connect(
|
||||
'notify::source', self._on_new_source)
|
||||
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
|
||||
self._connect(playbin, 'notify::source', self._on_new_source)
|
||||
|
||||
self._playbin = playbin
|
||||
|
||||
def _on_about_to_finish(self, element):
|
||||
self._appsrc = None
|
||||
source, self._appsrc = self._appsrc, None
|
||||
if source is None:
|
||||
return
|
||||
self._appsrc_caps = None
|
||||
|
||||
self._disconnect(source, 'need-data')
|
||||
self._disconnect(source, 'enough-data')
|
||||
self._disconnect(source, 'seek-data')
|
||||
|
||||
def _on_new_source(self, element, pad):
|
||||
uri = element.get_property('uri')
|
||||
if not uri or not uri.startswith('appsrc://'):
|
||||
return
|
||||
|
||||
# These caps matches the audio data provided by libspotify
|
||||
default_caps = gst.Caps(
|
||||
b'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
|
||||
b'width=(int)16, depth=(int)16, signed=(boolean)true, '
|
||||
b'rate=(int)44100')
|
||||
source = element.get_property('source')
|
||||
source.set_property('caps', default_caps)
|
||||
# GStreamer does not like unicode
|
||||
source.set_property('caps', self._appsrc_caps)
|
||||
source.set_property('format', b'time')
|
||||
source.set_property('stream-type', b'seekable')
|
||||
source.set_property('max-bytes', 1 * MB)
|
||||
source.set_property('min-percent', 50)
|
||||
|
||||
self._connect(source, 'need-data', self._appsrc_on_need_data)
|
||||
self._connect(source, 'enough-data', self._appsrc_on_enough_data)
|
||||
self._connect(source, 'seek-data', self._appsrc_on_seek_data)
|
||||
|
||||
self._appsrc = source
|
||||
|
||||
def _appsrc_on_need_data(self, appsrc, gst_length_hint):
|
||||
length_hint = utils.clocktime_to_millisecond(gst_length_hint)
|
||||
if self._appsrc_need_data_callback is not None:
|
||||
self._appsrc_need_data_callback(length_hint)
|
||||
return True
|
||||
|
||||
def _appsrc_on_enough_data(self, appsrc):
|
||||
if self._appsrc_enough_data_callback is not None:
|
||||
self._appsrc_enough_data_callback()
|
||||
return True
|
||||
|
||||
def _appsrc_on_seek_data(self, appsrc, gst_position):
|
||||
position = utils.clocktime_to_millisecond(gst_position)
|
||||
if self._appsrc_seek_data_callback is not None:
|
||||
self._appsrc_seek_data_callback(position)
|
||||
return True
|
||||
|
||||
def _teardown_playbin(self):
|
||||
if self._about_to_finish_id:
|
||||
self._playbin.disconnect(self._about_to_finish_id)
|
||||
if self._notify_source_signal_id:
|
||||
self._playbin.disconnect(self._notify_source_signal_id)
|
||||
self._disconnect(self._playbin, 'about-to-finish')
|
||||
self._disconnect(self._playbin, 'notify::source')
|
||||
self._playbin.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_output(self):
|
||||
@ -183,28 +222,34 @@ class Audio(pykka.ThreadingActor):
|
||||
def _setup_message_processor(self):
|
||||
bus = self._playbin.get_bus()
|
||||
bus.add_signal_watch()
|
||||
self._message_signal_id = bus.connect('message', self._on_message)
|
||||
self._connect(bus, 'message', self._on_message)
|
||||
|
||||
def _teardown_message_processor(self):
|
||||
if self._message_signal_id:
|
||||
bus = self._playbin.get_bus()
|
||||
bus.disconnect(self._message_signal_id)
|
||||
bus.remove_signal_watch()
|
||||
bus = self._playbin.get_bus()
|
||||
self._disconnect(bus, 'message')
|
||||
bus.remove_signal_watch()
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
if (message.type == gst.MESSAGE_STATE_CHANGED
|
||||
and message.src == self._playbin):
|
||||
old_state, new_state, pending_state = message.parse_state_changed()
|
||||
self._on_playbin_state_changed(old_state, new_state, pending_state)
|
||||
elif message.type == gst.MESSAGE_BUFFERING:
|
||||
percent = message.parse_buffering()
|
||||
logger.debug('Buffer %d%% full', percent)
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
self._on_end_of_stream()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
error, debug = message.parse_error()
|
||||
logger.error('%s %s', error, debug)
|
||||
logger.error(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
self.stop_playback()
|
||||
elif message.type == gst.MESSAGE_WARNING:
|
||||
error, debug = message.parse_warning()
|
||||
logger.warning('%s %s', error, debug)
|
||||
logger.warning(
|
||||
'%s Debug message: %s',
|
||||
str(error).decode('utf-8'), debug.decode('utf-8') or 'None')
|
||||
|
||||
def _on_playbin_state_changed(self, old_state, new_state, pending_state):
|
||||
if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
|
||||
@ -250,6 +295,32 @@ class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
self._playbin.set_property('uri', uri)
|
||||
|
||||
def set_appsrc(
|
||||
self, caps, need_data=None, enough_data=None, seek_data=None):
|
||||
"""
|
||||
Switch to using appsrc for getting audio to be played.
|
||||
|
||||
You *MUST* call :meth:`prepare_change` before calling this method.
|
||||
|
||||
:param caps: GStreamer caps string describing the audio format to
|
||||
expect
|
||||
:type caps: string
|
||||
:param need_data: callback for when appsrc needs data
|
||||
:type need_data: callable which takes data length hint in ms
|
||||
:param enough_data: callback for when appsrc has enough data
|
||||
:type enough_data: callable
|
||||
:param seek_data: callback for when data from a new position is needed
|
||||
to continue playback
|
||||
:type seek_data: callable which takes time position in ms
|
||||
"""
|
||||
if isinstance(caps, unicode):
|
||||
caps = caps.encode('utf-8')
|
||||
self._appsrc_caps = gst.Caps(caps)
|
||||
self._appsrc_need_data_callback = need_data
|
||||
self._appsrc_enough_data_callback = enough_data
|
||||
self._appsrc_seek_data_callback = seek_data
|
||||
self._playbin.set_property('uri', 'appsrc://')
|
||||
|
||||
def emit_data(self, buffer_):
|
||||
"""
|
||||
Call this to deliver raw audio data to be played.
|
||||
@ -282,13 +353,11 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
if self._playbin.get_state()[1] == gst.STATE_NULL:
|
||||
return 0
|
||||
try:
|
||||
position = self._playbin.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
except gst.QueryError, e:
|
||||
logger.error('time_position failed: %s', e)
|
||||
gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0]
|
||||
return utils.clocktime_to_millisecond(gst_position)
|
||||
except gst.QueryError:
|
||||
logger.debug('Position query failed')
|
||||
return 0
|
||||
|
||||
def set_position(self, position):
|
||||
@ -299,12 +368,9 @@ class Audio(pykka.ThreadingActor):
|
||||
:type position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self._playbin.get_state() # block until state changes are done
|
||||
handeled = self._playbin.seek_simple(
|
||||
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH,
|
||||
position * gst.MSECOND)
|
||||
self._playbin.get_state() # block until seek is done
|
||||
return handeled
|
||||
gst_position = utils.millisecond_to_clocktime(position)
|
||||
return self._playbin.seek_simple(
|
||||
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position)
|
||||
|
||||
def start_playback(self):
|
||||
"""
|
||||
|
||||
68
mopidy/audio/dummy.py
Normal file
68
mopidy/audio/dummy.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""A dummy audio actor for use in tests.
|
||||
|
||||
This class implements the audio API in the simplest way possible. It is used in
|
||||
tests of the core and backends.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from .constants import PlaybackState
|
||||
from .listener import AudioListener
|
||||
|
||||
|
||||
class DummyAudio(pykka.ThreadingActor):
|
||||
def __init__(self):
|
||||
super(DummyAudio, self).__init__()
|
||||
self.state = PlaybackState.STOPPED
|
||||
self._position = 0
|
||||
|
||||
def set_on_end_of_track(self, callback):
|
||||
pass
|
||||
|
||||
def set_uri(self, uri):
|
||||
pass
|
||||
|
||||
def set_appsrc(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def emit_data(self, buffer_):
|
||||
pass
|
||||
|
||||
def emit_end_of_stream(self):
|
||||
pass
|
||||
|
||||
def get_position(self):
|
||||
return self._position
|
||||
|
||||
def set_position(self, position):
|
||||
self._position = position
|
||||
return True
|
||||
|
||||
def start_playback(self):
|
||||
return self._change_state(PlaybackState.PLAYING)
|
||||
|
||||
def pause_playback(self):
|
||||
return self._change_state(PlaybackState.PAUSED)
|
||||
|
||||
def prepare_change(self):
|
||||
return True
|
||||
|
||||
def stop_playback(self):
|
||||
return self._change_state(PlaybackState.STOPPED)
|
||||
|
||||
def get_volume(self):
|
||||
return 0
|
||||
|
||||
def set_volume(self, volume):
|
||||
pass
|
||||
|
||||
def set_metadata(self, track):
|
||||
pass
|
||||
|
||||
def _change_state(self, new_state):
|
||||
old_state, self.state = self.state, new_state
|
||||
AudioListener.send(
|
||||
'state_changed', old_state=old_state, new_state=new_state)
|
||||
return True
|
||||
@ -5,6 +5,39 @@ pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
|
||||
def calculate_duration(num_samples, sample_rate):
|
||||
"""Determine duration of samples using GStreamer helper for precise
|
||||
math."""
|
||||
return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate)
|
||||
|
||||
|
||||
def create_buffer(data, capabilites=None, timestamp=None, duration=None):
|
||||
"""Create a new GStreamer buffer based on provided data.
|
||||
|
||||
Mainly intended to keep gst imports out of non-audio modules.
|
||||
"""
|
||||
buffer_ = gst.Buffer(data)
|
||||
if capabilites:
|
||||
if isinstance(capabilites, basestring):
|
||||
capabilites = gst.caps_from_string(capabilites)
|
||||
buffer_.set_caps(capabilites)
|
||||
if timestamp:
|
||||
buffer_.timestamp = timestamp
|
||||
if duration:
|
||||
buffer_.duration = duration
|
||||
return buffer_
|
||||
|
||||
|
||||
def millisecond_to_clocktime(value):
|
||||
"""Convert a millisecond time to internal GStreamer time."""
|
||||
return value * gst.MSECOND
|
||||
|
||||
|
||||
def clocktime_to_millisecond(value):
|
||||
"""Convert an internal GStreamer time to millisecond time."""
|
||||
return value // gst.MSECOND
|
||||
|
||||
|
||||
def supported_uri_schemes(uri_schemes):
|
||||
"""Determine which URIs we can actually support from provided whitelist.
|
||||
|
||||
@ -12,7 +45,7 @@ def supported_uri_schemes(uri_schemes):
|
||||
:type uri_schemes: list or set or URI schemes as strings.
|
||||
:rtype: set of URI schemes we can support via this GStreamer install.
|
||||
"""
|
||||
supported_schemes= set()
|
||||
supported_schemes = set()
|
||||
registry = gst.registry_get_default()
|
||||
|
||||
for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY):
|
||||
|
||||
@ -53,7 +53,7 @@ class BaseLibraryProvider(object):
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def find_exact(self, **query):
|
||||
def find_exact(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.find_exact`.
|
||||
|
||||
@ -77,7 +77,7 @@ class BaseLibraryProvider(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def search(self, **query):
|
||||
def search(self, query=None, uris=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.search`.
|
||||
|
||||
|
||||
@ -35,7 +35,11 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
logger.debug('Failed to lookup %r', uri)
|
||||
return []
|
||||
|
||||
def find_exact(self, **query):
|
||||
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()
|
||||
|
||||
@ -72,7 +76,11 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
return SearchResult(uri='file:search', tracks=result_tracks)
|
||||
|
||||
def search(self, **query):
|
||||
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()
|
||||
|
||||
|
||||
@ -142,12 +142,6 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
albumartist_kwargs[b'musicbrainz_id'] = (
|
||||
data['musicbrainz_albumartistid'])
|
||||
|
||||
if data['file'][0] == '/':
|
||||
path = data['file'][1:]
|
||||
else:
|
||||
path = data['file']
|
||||
path = urllib.unquote(path)
|
||||
|
||||
if artist_kwargs:
|
||||
artist = Artist(**artist_kwargs)
|
||||
track_kwargs[b'artists'] = [artist]
|
||||
@ -160,7 +154,19 @@ def _convert_mpd_data(data, tracks, music_dir):
|
||||
album = Album(**album_kwargs)
|
||||
track_kwargs[b'album'] = album
|
||||
|
||||
if data['file'][0] == '/':
|
||||
path = data['file'][1:]
|
||||
else:
|
||||
path = data['file']
|
||||
path = urllib.unquote(path.encode('utf-8'))
|
||||
|
||||
if isinstance(music_dir, unicode):
|
||||
music_dir = music_dir.encode('utf-8')
|
||||
|
||||
# Make sure we only pass bytestrings to path_to_uri to avoid implicit
|
||||
# decoding of bytestrings to unicode strings
|
||||
track_kwargs[b'uri'] = path_to_uri(music_dir, path)
|
||||
|
||||
track_kwargs[b'length'] = int(data.get('time', 0)) * 1000
|
||||
|
||||
track = Track(**track_kwargs)
|
||||
|
||||
@ -62,8 +62,8 @@ class SpotifyTrack(Track):
|
||||
|
||||
|
||||
class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
def find_exact(self, **query):
|
||||
return self.search(**query)
|
||||
def find_exact(self, query=None, uris=None):
|
||||
return self.search(query=query, uris=uris)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
@ -131,7 +131,9 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
def refresh(self, uri=None):
|
||||
pass # TODO
|
||||
|
||||
def search(self, **query):
|
||||
def search(self, query=None, uris=None):
|
||||
# TODO Only return results within URI roots given by ``uris``
|
||||
|
||||
if not query:
|
||||
return self._get_all_tracks()
|
||||
|
||||
|
||||
@ -1,113 +1,94 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import time
|
||||
import functools
|
||||
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy import audio
|
||||
from mopidy.backends import base
|
||||
from mopidy.core import PlaybackState
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
|
||||
def need_data_callback(spotify_backend, length_hint):
|
||||
spotify_backend.playback.on_need_data(length_hint)
|
||||
|
||||
|
||||
def enough_data_callback(spotify_backend):
|
||||
spotify_backend.playback.on_enough_data()
|
||||
|
||||
|
||||
def seek_data_callback(spotify_backend, time_position):
|
||||
spotify_backend.playback.on_seek_data(time_position)
|
||||
|
||||
|
||||
class SpotifyPlaybackProvider(base.BasePlaybackProvider):
|
||||
# These GStreamer caps matches the audio data provided by libspotify
|
||||
_caps = (
|
||||
'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
|
||||
'width=(int)16, depth=(int)16, signed=(boolean)true, '
|
||||
'rate=(int)44100')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||
|
||||
self._timer = TrackPositionTimer()
|
||||
|
||||
def pause(self):
|
||||
self._timer.pause()
|
||||
|
||||
return super(SpotifyPlaybackProvider, self).pause()
|
||||
self._first_seek = False
|
||||
|
||||
def play(self, track):
|
||||
if track.uri is None:
|
||||
return False
|
||||
|
||||
spotify_backend = self.backend.actor_ref.proxy()
|
||||
need_data_callback_bound = functools.partial(
|
||||
need_data_callback, spotify_backend)
|
||||
enough_data_callback_bound = functools.partial(
|
||||
enough_data_callback, spotify_backend)
|
||||
seek_data_callback_bound = functools.partial(
|
||||
seek_data_callback, spotify_backend)
|
||||
|
||||
self._first_seek = True
|
||||
|
||||
try:
|
||||
self.backend.spotify.session.load(
|
||||
Link.from_string(track.uri).as_track())
|
||||
self.backend.spotify.session.play(1)
|
||||
self.backend.spotify.buffer_timestamp = 0
|
||||
|
||||
self.audio.prepare_change()
|
||||
self.audio.set_uri('appsrc://')
|
||||
self.audio.set_appsrc(
|
||||
self._caps,
|
||||
need_data=need_data_callback_bound,
|
||||
enough_data=enough_data_callback_bound,
|
||||
seek_data=seek_data_callback_bound)
|
||||
self.audio.start_playback()
|
||||
self.audio.set_metadata(track)
|
||||
|
||||
self._timer.play()
|
||||
|
||||
return True
|
||||
except SpotifyError as e:
|
||||
logger.info('Playback of %s failed: %s', track.uri, e)
|
||||
return False
|
||||
|
||||
def resume(self):
|
||||
time_position = self.get_time_position()
|
||||
self._timer.resume()
|
||||
self.audio.prepare_change()
|
||||
result = self.seek(time_position)
|
||||
self.audio.start_playback()
|
||||
return result
|
||||
|
||||
def seek(self, time_position):
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
self._timer.seek(time_position)
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
self.backend.spotify.session.play(0)
|
||||
|
||||
return super(SpotifyPlaybackProvider, self).stop()
|
||||
|
||||
def get_time_position(self):
|
||||
# XXX: The default implementation of get_time_position hangs/times out
|
||||
# when used with the Spotify backend and GStreamer appsrc. If this can
|
||||
# be resolved, we no longer need to use a wall clock based time
|
||||
# position for Spotify playback.
|
||||
return self._timer.get_time_position()
|
||||
def on_need_data(self, length_hint):
|
||||
logger.debug('playback.on_need_data(%d) called', length_hint)
|
||||
self.backend.spotify.push_audio_data = True
|
||||
|
||||
def on_enough_data(self):
|
||||
logger.debug('playback.on_enough_data() called')
|
||||
self.backend.spotify.push_audio_data = False
|
||||
|
||||
class TrackPositionTimer(object):
|
||||
"""
|
||||
Keeps track of time position in a track using the wall clock and playback
|
||||
events.
|
||||
def on_seek_data(self, time_position):
|
||||
logger.debug('playback.on_seek_data(%d) called', time_position)
|
||||
|
||||
To not introduce a reverse dependency on the playback controller, this
|
||||
class keeps track of playback state itself.
|
||||
"""
|
||||
if time_position == 0 and self._first_seek:
|
||||
self._first_seek = False
|
||||
logger.debug('Skipping seek due to issue #300')
|
||||
return
|
||||
|
||||
def __init__(self):
|
||||
self._state = PlaybackState.STOPPED
|
||||
self._accumulated = 0
|
||||
self._started = 0
|
||||
|
||||
def play(self):
|
||||
self._state = PlaybackState.PLAYING
|
||||
self._accumulated = 0
|
||||
self._started = self._wall_time()
|
||||
|
||||
def pause(self):
|
||||
self._state = PlaybackState.PAUSED
|
||||
self._accumulated += self._wall_time() - self._started
|
||||
|
||||
def resume(self):
|
||||
self._state = PlaybackState.PLAYING
|
||||
|
||||
def seek(self, time_position):
|
||||
self._started = self._wall_time()
|
||||
self._accumulated = time_position
|
||||
|
||||
def get_time_position(self):
|
||||
if self._state == PlaybackState.PLAYING:
|
||||
time_since_started = self._wall_time() - self._started
|
||||
return self._accumulated + time_since_started
|
||||
elif self._state == PlaybackState.PAUSED:
|
||||
return self._accumulated
|
||||
elif self._state == PlaybackState.STOPPED:
|
||||
return 0
|
||||
|
||||
def _wall_time(self):
|
||||
return int(time.time() * 1000)
|
||||
self.backend.spotify.buffer_timestamp = audio.millisecond_to_clocktime(
|
||||
time_position)
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
@ -46,6 +42,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
self.backend_ref = backend_ref
|
||||
|
||||
self.connected = threading.Event()
|
||||
self.push_audio_data = True
|
||||
self.buffer_timestamp = 0
|
||||
|
||||
self.container_manager = None
|
||||
self.playlist_manager = None
|
||||
@ -107,6 +105,10 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
"""Callback used by pyspotify"""
|
||||
# pylint: disable = R0913
|
||||
# Too many arguments (8/5)
|
||||
|
||||
if not self.push_audio_data:
|
||||
return 0
|
||||
|
||||
assert sample_type == 0, 'Expects 16-bit signed integer samples'
|
||||
capabilites = """
|
||||
audio/x-raw-int,
|
||||
@ -120,8 +122,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
'sample_rate': sample_rate,
|
||||
'channels': channels,
|
||||
}
|
||||
buffer_ = gst.Buffer(bytes(frames))
|
||||
buffer_.set_caps(gst.caps_from_string(capabilites))
|
||||
|
||||
duration = audio.calculate_duration(num_frames, sample_rate)
|
||||
buffer_ = audio.create_buffer(bytes(frames),
|
||||
capabilites=capabilites,
|
||||
timestamp=self.buffer_timestamp,
|
||||
duration=duration)
|
||||
|
||||
self.buffer_timestamp += duration
|
||||
|
||||
if self.audio.emit_data(buffer_).get():
|
||||
return num_frames
|
||||
|
||||
@ -70,13 +70,16 @@ def to_mopidy_playlist(spotify_playlist):
|
||||
uri = str(Link.from_playlist(spotify_playlist))
|
||||
if not spotify_playlist.is_loaded():
|
||||
return Playlist(uri=uri, name='[loading...]')
|
||||
if not spotify_playlist.name():
|
||||
name = spotify_playlist.name()
|
||||
if not name:
|
||||
# Other user's "starred" playlists isn't handled properly by pyspotify
|
||||
# See https://github.com/mopidy/pyspotify/issues/81
|
||||
return
|
||||
if spotify_playlist.owner().canonical_name() != settings.SPOTIFY_USERNAME:
|
||||
name += ' by ' + spotify_playlist.owner().canonical_name()
|
||||
return Playlist(
|
||||
uri=uri,
|
||||
name=spotify_playlist.name(),
|
||||
name=name,
|
||||
tracks=[
|
||||
to_mopidy_track(spotify_track)
|
||||
for spotify_track in spotify_playlist
|
||||
|
||||
@ -5,10 +5,9 @@ import urlparse
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.audio import utils
|
||||
from mopidy import audio as audio_lib, settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import SearchResult, Track
|
||||
from mopidy.models import Track
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.stream')
|
||||
|
||||
@ -21,7 +20,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend):
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.playlists = None
|
||||
|
||||
self.uri_schemes = utils.supported_uri_schemes(
|
||||
self.uri_schemes = audio_lib.supported_uri_schemes(
|
||||
settings.STREAM_PROTOCOLS)
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
@ -16,31 +17,60 @@ class LibraryController(object):
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.with_library_by_uri_scheme.get(uri_scheme, None)
|
||||
|
||||
def find_exact(self, query=None, **kwargs):
|
||||
def _get_backends_to_uris(self, uris):
|
||||
if uris:
|
||||
backends_to_uris = 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])
|
||||
return backends_to_uris
|
||||
|
||||
def find_exact(self, query=None, uris=None, **kwargs):
|
||||
"""
|
||||
Search the library for tracks where ``field`` is ``values``.
|
||||
|
||||
If the query is empty, and the backend can support it, all available
|
||||
tracks are returned.
|
||||
|
||||
If ``uris`` is given, the search is limited to results from within the
|
||||
URI roots. For example passing ``uris=['file:']`` will limit the search
|
||||
to the local backend.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
# Returns results matching 'a' from any backend
|
||||
find_exact({'any': ['a']})
|
||||
find_exact(any=['a'])
|
||||
|
||||
# Returns results matching artist 'xyz'
|
||||
# Returns results matching artist 'xyz' from any backend
|
||||
find_exact({'artist': ['xyz']})
|
||||
find_exact(artist=['xyz'])
|
||||
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz' from any
|
||||
# backend
|
||||
find_exact({'any': ['a', 'b'], 'artist': ['xyz']})
|
||||
find_exact(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
# Returns results matching 'a' if within the given URI roots
|
||||
# "file:///media/music" and "spotify:"
|
||||
find_exact(
|
||||
{'any': ['a']}, uris=['file:///media/music', 'spotify:'])
|
||||
find_exact(any=['a'], uris=['file:///media/music', 'spotify:'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:param uris: zero or more URI roots to limit the search to
|
||||
:type uris: list of strings or :class:`None`
|
||||
:rtype: list of :class:`mopidy.models.SearchResult`
|
||||
"""
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.find_exact(**query) for b in self.backends.with_library]
|
||||
backend.library.find_exact(query=query, uris=uris)
|
||||
for (backend, uris) in self._get_backends_to_uris(uris).items()]
|
||||
return [result for result in pykka.get_all(futures) if result]
|
||||
|
||||
def lookup(self, uri):
|
||||
@ -76,29 +106,45 @@ class LibraryController(object):
|
||||
b.library.refresh(uri) for b in self.backends.with_library]
|
||||
pykka.get_all(futures)
|
||||
|
||||
def search(self, query=None, **kwargs):
|
||||
def search(self, query=None, uris=None, **kwargs):
|
||||
"""
|
||||
Search the library for tracks where ``field`` contains ``values``.
|
||||
|
||||
If the query is empty, and the backend can support it, all available
|
||||
tracks are returned.
|
||||
|
||||
If ``uris`` is given, the search is limited to results from within the
|
||||
URI roots. For example passing ``uris=['file:']`` will limit the search
|
||||
to the local backend.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
# Returns results matching 'a' in any backend
|
||||
search({'any': ['a']})
|
||||
search(any=['a'])
|
||||
|
||||
# Returns results matching artist 'xyz'
|
||||
# Returns results matching artist 'xyz' in any backend
|
||||
search({'artist': ['xyz']})
|
||||
search(artist=['xyz'])
|
||||
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz' in any
|
||||
# backend
|
||||
search({'any': ['a', 'b'], 'artist': ['xyz']})
|
||||
search(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
# Returns results matching 'a' if within the given URI roots
|
||||
# "file:///media/music" and "spotify:"
|
||||
search({'any': ['a']}, uris=['file:///media/music', 'spotify:'])
|
||||
search(any=['a'], uris=['file:///media/music', 'spotify:'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:param uris: zero or more URI roots to limit the search to
|
||||
:type uris: list of strings or :class:`None`
|
||||
:rtype: list of :class:`mopidy.models.SearchResult`
|
||||
"""
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.search(**query) for b in self.backends.with_library]
|
||||
backend.library.search(query=query, uris=uris)
|
||||
for (backend, uris) in self._get_backends_to_uris(uris).items()]
|
||||
return [result for result in pykka.get_all(futures) if result]
|
||||
|
||||
@ -397,13 +397,14 @@ class PlaybackController(object):
|
||||
self.state = PlaybackState.PLAYING
|
||||
backend = self._get_backend()
|
||||
if not backend or not backend.playback.play(tl_track.track).get():
|
||||
# Track is not playable
|
||||
logger.warning('Track is not playable: %s', tl_track.track.uri)
|
||||
if self.random and self._shuffled:
|
||||
self._shuffled.remove(tl_track)
|
||||
if on_error_step == 1:
|
||||
self.next()
|
||||
elif on_error_step == -1:
|
||||
self.previous()
|
||||
return
|
||||
|
||||
if self.random and self.current_tl_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_tl_track)
|
||||
|
||||
@ -62,10 +62,13 @@ class TracklistController(object):
|
||||
Is not reset before Mopidy is restarted.
|
||||
"""
|
||||
|
||||
def add(self, tracks, at_position=None):
|
||||
def add(self, tracks=None, at_position=None, uri=None):
|
||||
"""
|
||||
Add the track or list of tracks to the tracklist.
|
||||
|
||||
If ``uri`` is given instead of ``tracks``, the URI is looked up in the
|
||||
library and the resulting tracks are added to the tracklist.
|
||||
|
||||
If ``at_position`` is given, the tracks placed at the given position in
|
||||
the tracklist. If ``at_position`` is not given, the tracks are appended
|
||||
to the end of the tracklist.
|
||||
@ -76,9 +79,18 @@ class TracklistController(object):
|
||||
:type tracks: list of :class:`mopidy.models.Track`
|
||||
:param at_position: position in tracklist to add track
|
||||
:type at_position: int or :class:`None`
|
||||
:param uri: URI for tracks to add
|
||||
:type uri: string
|
||||
:rtype: list of :class:`mopidy.models.TlTrack`
|
||||
"""
|
||||
assert tracks is not None or uri is not None, \
|
||||
'tracks or uri must be provided'
|
||||
|
||||
if tracks is None and uri is not None:
|
||||
tracks = self._core.library.lookup(uri)
|
||||
|
||||
tl_tracks = []
|
||||
|
||||
for track in tracks:
|
||||
tl_track = TlTrack(self._next_tlid, track)
|
||||
self._next_tlid += 1
|
||||
|
||||
@ -128,8 +128,8 @@ you quickly started with working on your client instead of figuring out how to
|
||||
communicate with Mopidy.
|
||||
|
||||
|
||||
Getting the library
|
||||
-------------------
|
||||
Getting the library for browser use
|
||||
-----------------------------------
|
||||
|
||||
Regular and minified versions of Mopidy.js, ready for use, is installed
|
||||
together with Mopidy. When the HTTP frontend is running, the files are
|
||||
@ -154,9 +154,28 @@ the Git repo at:
|
||||
- ``mopidy/frontends/http/data/mopidy.js``
|
||||
- ``mopidy/frontends/http/data/mopidy.min.js``
|
||||
|
||||
|
||||
Getting the library for Node.js use
|
||||
-----------------------------------
|
||||
|
||||
If you want to use Mopidy.js from Node.js instead of a browser, you can install
|
||||
Mopidy.js using npm::
|
||||
|
||||
npm install mopidy
|
||||
|
||||
After npm completes, you can import Mopidy.js using ``require()``:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var Mopidy = require("mopidy").Mopidy;
|
||||
|
||||
|
||||
Getting the library for development on the library
|
||||
--------------------------------------------------
|
||||
|
||||
If you want to work on the Mopidy.js library itself, you'll find a complete
|
||||
development setup in the ``js/`` dir in our repo. The instructions in
|
||||
``js/README.rst`` will guide you on your way.
|
||||
``js/README.md`` will guide you on your way.
|
||||
|
||||
|
||||
Creating an instance
|
||||
@ -170,8 +189,8 @@ Once you got Mopidy.js loaded, you need to create an instance of the wrapper:
|
||||
|
||||
When you instantiate ``Mopidy()`` without arguments, it will connect to
|
||||
the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host
|
||||
your web client using Mopidy's web server, you'll need to pass the URL to the
|
||||
WebSocket end point:
|
||||
your web client using Mopidy's web server, or if you use Mopidy.js from a
|
||||
Node.js environment, you'll need to pass the URL to the WebSocket end point:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4
mopidy/frontends/http/data/mopidy.min.js
vendored
4
mopidy/frontends/http/data/mopidy.min.js
vendored
File diff suppressed because one or more lines are too long
@ -20,6 +20,7 @@ class WebSocketResource(object):
|
||||
self._core = core_proxy
|
||||
inspector = jsonrpc.JsonRpcInspector(
|
||||
objects={
|
||||
'core.get_uri_schemes': core.Core.get_uri_schemes,
|
||||
'core.library': core.LibraryController,
|
||||
'core.playback': core.PlaybackController,
|
||||
'core.playlists': core.PlaylistsController,
|
||||
@ -28,6 +29,7 @@ class WebSocketResource(object):
|
||||
self.jsonrpc = jsonrpc.JsonRpcWrapper(
|
||||
objects={
|
||||
'core.describe': inspector.describe,
|
||||
'core.get_uri_schemes': self._core.get_uri_schemes,
|
||||
'core.library': self._core.library,
|
||||
'core.playback': self._core.playback,
|
||||
'core.playlists': self._core.playlists,
|
||||
|
||||
@ -22,11 +22,9 @@ def add(context, uri):
|
||||
"""
|
||||
if not uri:
|
||||
return
|
||||
tracks = context.core.library.lookup(uri).get()
|
||||
if tracks:
|
||||
context.core.tracklist.add(tracks)
|
||||
return
|
||||
raise MpdNoExistError('directory or file not found', command='add')
|
||||
tl_tracks = context.core.tracklist.add(uri=uri).get()
|
||||
if not tl_tracks:
|
||||
raise MpdNoExistError('directory or file not found', command='add')
|
||||
|
||||
|
||||
@handle_request(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
@ -52,12 +50,11 @@ def addid(context, uri, songpos=None):
|
||||
raise MpdNoExistError('No such song', command='addid')
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
tracks = context.core.library.lookup(uri).get()
|
||||
if not tracks:
|
||||
raise MpdNoExistError('No such song', command='addid')
|
||||
if songpos and songpos > context.core.tracklist.length.get():
|
||||
raise MpdArgError('Bad song index', command='addid')
|
||||
tl_tracks = context.core.tracklist.add(tracks, at_position=songpos).get()
|
||||
tl_tracks = context.core.tracklist.add(uri=uri, at_position=songpos).get()
|
||||
if not tl_tracks:
|
||||
raise MpdNoExistError('No such song', command='addid')
|
||||
return ('Id', tl_tracks[0].tlid)
|
||||
|
||||
|
||||
|
||||
@ -279,9 +279,8 @@ class MprisObject(dbus.service.Object):
|
||||
return
|
||||
# NOTE Check if URI has MIME type known to the backend, if MIME support
|
||||
# is added to the backend.
|
||||
tracks = self.core.library.lookup(uri).get()
|
||||
if tracks:
|
||||
tl_tracks = self.core.tracklist.add(tracks).get()
|
||||
tl_tracks = self.core.tracklist.add(uri=uri).get()
|
||||
if tl_tracks:
|
||||
self.core.playback.play(tl_tracks[0])
|
||||
else:
|
||||
logger.debug('Track with URI "%s" not found in library.', uri)
|
||||
@ -375,6 +374,12 @@ class MprisObject(dbus.service.Object):
|
||||
artists.sort(key=lambda a: a.name)
|
||||
metadata['xesam:albumArtist'] = dbus.Array(
|
||||
[a.name for a in artists if a.name], signature='s')
|
||||
if track.album and track.album.images:
|
||||
url = list(track.album.images)[0]
|
||||
if url:
|
||||
metadata['mpris:artUrl'] = url
|
||||
if track.disc_no:
|
||||
metadata['xesam:discNumber'] = track.disc_no
|
||||
if track.track_no:
|
||||
metadata['xesam:trackNumber'] = track.track_no
|
||||
return dbus.Dictionary(metadata, signature='sv')
|
||||
|
||||
@ -203,6 +203,9 @@ class Album(ImmutableObject):
|
||||
|
||||
#: The album image URIs. Read-only.
|
||||
images = frozenset()
|
||||
# XXX If we want to keep the order of images we shouldn't use frozenset()
|
||||
# as it doesn't preserve order. I'm deferring this issue until we got
|
||||
# actual usage of this field with more than one image.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
|
||||
|
||||
@ -153,7 +153,8 @@ class Scanner(object):
|
||||
self.fakesink.connect('handoff', self.process_handoff)
|
||||
|
||||
self.uribin = gst.element_factory_make('uridecodebin')
|
||||
self.uribin.set_property('caps', gst.Caps(b'audio/x-raw-int'))
|
||||
self.uribin.set_property('caps',
|
||||
gst.Caps(b'audio/x-raw-int; audio/x-raw-float'))
|
||||
self.uribin.connect('pad-added', self.process_new_pad)
|
||||
|
||||
self.pipe = gst.element_factory_make('pipeline')
|
||||
|
||||
@ -20,7 +20,7 @@ from __future__ import unicode_literals
|
||||
#: BACKENDS = (
|
||||
#: u'mopidy.backends.local.LocalBackend',
|
||||
#: u'mopidy.backends.spotify.SpotifyBackend',
|
||||
#: u'mopidy.backends.spotify.StreamBackend',
|
||||
#: u'mopidy.backends.stream.StreamBackend',
|
||||
#: )
|
||||
BACKENDS = (
|
||||
'mopidy.backends.local.LocalBackend',
|
||||
@ -49,14 +49,6 @@ DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \
|
||||
#: DEBUG_LOG_FILENAME = u'mopidy.log'
|
||||
DEBUG_LOG_FILENAME = 'mopidy.log'
|
||||
|
||||
#: If we should start a background thread that dumps thread's traceback when we
|
||||
#: get a SIGUSR1. Mainly a debug tool for figuring out deadlocks.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: DEBUG_THREAD = False
|
||||
DEBUG_THREAD = False
|
||||
|
||||
#: Location of the Mopidy .desktop file.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.mpris`.
|
||||
|
||||
@ -181,7 +181,7 @@ class JsonRpcWrapper(object):
|
||||
data='"params", if given, must be an array or an object')
|
||||
|
||||
def _get_method(self, method_path):
|
||||
if inspect.isroutine(self.objects.get(method_path, None)):
|
||||
if callable(self.objects.get(method_path, None)):
|
||||
# The mounted object is the callable
|
||||
return self.objects[method_path]
|
||||
|
||||
|
||||
@ -2,10 +2,8 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import thread
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from pykka import ActorDeadError
|
||||
from pykka.registry import ActorRegistry
|
||||
@ -79,29 +77,3 @@ class BaseThread(threading.Thread):
|
||||
|
||||
def run_inside_try(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DebugThread(threading.Thread):
|
||||
daemon = True
|
||||
name = 'DebugThread'
|
||||
|
||||
event = threading.Event()
|
||||
|
||||
def handler(self, signum, frame):
|
||||
logger.info('Got %s signal', SIGNALS[signum])
|
||||
self.event.set()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.event.wait()
|
||||
threads = dict((t.ident, t.name) for t in threading.enumerate())
|
||||
|
||||
for ident, frame in sys._current_frames().items():
|
||||
if self.ident != ident:
|
||||
stack = ''.join(traceback.format_stack(frame))
|
||||
logger.debug(
|
||||
'Current state of %s (%s):\n%s',
|
||||
threads.get(ident, '?'), ident, stack)
|
||||
del frame
|
||||
|
||||
self.event.clear()
|
||||
|
||||
@ -142,7 +142,13 @@ def validate_settings(defaults, settings):
|
||||
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
|
||||
}
|
||||
|
||||
list_of_one_or_more = [
|
||||
must_be_iterable = [
|
||||
'BACKENDS',
|
||||
'FRONTENDS',
|
||||
'STREAM_PROTOCOLS',
|
||||
]
|
||||
|
||||
must_have_value_set = [
|
||||
'BACKENDS',
|
||||
'FRONTENDS',
|
||||
]
|
||||
@ -171,13 +177,13 @@ def validate_settings(defaults, settings):
|
||||
'Deprecated setting, please set the value via the GStreamer '
|
||||
'bin in OUTPUT.')
|
||||
|
||||
elif setting in list_of_one_or_more:
|
||||
if not hasattr(value, '__iter__'):
|
||||
errors[setting] = (
|
||||
'Must be a tuple. '
|
||||
"Remember the comma after single values: (u'value',)")
|
||||
if not value:
|
||||
errors[setting] = 'Must contain at least one value.'
|
||||
elif setting in must_be_iterable and not hasattr(value, '__iter__'):
|
||||
errors[setting] = (
|
||||
'Must be a tuple. '
|
||||
"Remember the comma after single values: (u'value',)")
|
||||
|
||||
elif setting in must_have_value_set and not value:
|
||||
errors[setting] = 'Must be set.'
|
||||
|
||||
elif setting not in defaults and not setting.startswith('CUSTOM_'):
|
||||
errors[setting] = 'Unknown setting.'
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
Pykka >= 1.0
|
||||
Pykka >= 1.1
|
||||
# Available as python-pykka from apt.mopidy.com
|
||||
|
||||
@ -10,7 +10,7 @@ from mopidy.backends import listener
|
||||
@mock.patch.object(listener.BackendListener, 'send')
|
||||
class BackendEventsTest(object):
|
||||
def setUp(self):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.audio = audio.DummyAudio.start().proxy()
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core.start(backends=[self.backend]).proxy()
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ import mock
|
||||
import random
|
||||
import time
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import audio, core
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import Track
|
||||
@ -18,7 +20,7 @@ class PlaybackControllerTest(object):
|
||||
tracks = []
|
||||
|
||||
def setUp(self):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.audio = audio.DummyAudio.start().proxy()
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
self.playback = self.core.playback
|
||||
@ -29,6 +31,9 @@ class PlaybackControllerTest(object):
|
||||
assert self.tracks[0].length >= 2000, \
|
||||
'First song needs to be at least 2000 miliseconds'
|
||||
|
||||
def tearDown(self):
|
||||
pykka.ActorRegistry.stop_all()
|
||||
|
||||
def test_initial_state_is_stopped(self):
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import mock
|
||||
import pykka
|
||||
|
||||
from mopidy import audio, core, settings
|
||||
@ -19,7 +18,7 @@ class PlaylistsControllerTest(object):
|
||||
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
|
||||
settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
|
||||
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.audio = audio.DummyAudio.start().proxy()
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import random
|
||||
|
||||
import pykka
|
||||
@ -16,9 +15,9 @@ class TracklistControllerTest(object):
|
||||
tracks = []
|
||||
|
||||
def setUp(self):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.audio = audio.DummyAudio.start().proxy()
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(audio=audio, backends=[self.backend])
|
||||
self.core = core.Core(audio=self.audio, backends=[self.backend])
|
||||
self.controller = self.core.tracklist
|
||||
self.playback = self.core.playback
|
||||
|
||||
|
||||
@ -87,8 +87,27 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library1.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
def test_find_exact_with_uris_selects_dummy1_backend(self):
|
||||
self.core.library.find_exact(
|
||||
any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy3:'])
|
||||
|
||||
self.library1.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
|
||||
self.assertFalse(self.library2.find_exact.called)
|
||||
|
||||
def test_find_exact_with_uris_selects_both_backends(self):
|
||||
self.core.library.find_exact(
|
||||
any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy2:'])
|
||||
|
||||
self.library1.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
|
||||
self.library2.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy2:'])
|
||||
|
||||
def test_find_exact_filters_out_none(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
@ -103,8 +122,10 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertNotIn(None, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library1.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
def test_find_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
@ -121,8 +142,10 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library1.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.find_exact.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
def test_search_combines_results_from_all_backends(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
@ -139,8 +162,27 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
self.library1.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
def test_search_with_uris_selects_dummy1_backend(self):
|
||||
self.core.library.search(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:'])
|
||||
|
||||
self.library1.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
|
||||
self.assertFalse(self.library2.search.called)
|
||||
|
||||
def test_search_with_uris_selects_both_backends(self):
|
||||
self.core.library.search(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy2:'])
|
||||
|
||||
self.library1.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
|
||||
self.library2.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=['dummy2:'])
|
||||
|
||||
def test_search_filters_out_none(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
@ -155,8 +197,10 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertNotIn(None, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
self.library1.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
def test_search_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
@ -173,5 +217,7 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
self.library1.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
self.library2.search.assert_called_once_with(
|
||||
query=dict(any=['a']), uris=None)
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.core import Core
|
||||
from mopidy.models import Track
|
||||
|
||||
@ -9,13 +12,31 @@ from tests import unittest
|
||||
class TracklistTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tracks = [
|
||||
Track(uri='a', name='foo'),
|
||||
Track(uri='b', name='foo'),
|
||||
Track(uri='c', name='bar')
|
||||
Track(uri='dummy1:a', name='foo'),
|
||||
Track(uri='dummy1:b', name='foo'),
|
||||
Track(uri='dummy1:c', name='bar'),
|
||||
]
|
||||
self.core = Core(audio=None, backends=[])
|
||||
|
||||
self.backend = mock.Mock()
|
||||
self.backend.uri_schemes.get.return_value = ['dummy1']
|
||||
self.library = mock.Mock(spec=base.BaseLibraryProvider)
|
||||
self.backend.library = self.library
|
||||
|
||||
self.core = Core(audio=None, backends=[self.backend])
|
||||
self.tl_tracks = self.core.tracklist.add(self.tracks)
|
||||
|
||||
def test_add_by_uri_looks_up_uri_in_library(self):
|
||||
track = Track(uri='dummy1:x', name='x')
|
||||
self.library.lookup().get.return_value = [track]
|
||||
self.library.lookup.reset_mock()
|
||||
|
||||
tl_tracks = self.core.tracklist.add(uri='dummy1:x')
|
||||
|
||||
self.library.lookup.assert_called_once_with('dummy1:x')
|
||||
self.assertEqual(1, len(tl_tracks))
|
||||
self.assertEqual(track, tl_tracks[0].track)
|
||||
self.assertEqual(tl_tracks, self.core.tracklist.tl_tracks[-1:])
|
||||
|
||||
def test_remove_removes_tl_tracks_matching_query(self):
|
||||
tl_tracks = self.core.tracklist.remove(name='foo')
|
||||
|
||||
|
||||
BIN
tests/data/scanner/simple/song1.ogg
Normal file
BIN
tests/data/scanner/simple/song1.ogg
Normal file
Binary file not shown.
@ -200,6 +200,37 @@ class PlayerInterfaceTest(unittest.TestCase):
|
||||
self.assertIn('xesam:albumArtist', result.keys())
|
||||
self.assertEqual(result['xesam:albumArtist'], ['a', 'b'])
|
||||
|
||||
def test_get_metadata_use_first_album_image_as_art_url(self):
|
||||
# XXX Currently, the album image order isn't preserved because they
|
||||
# are stored as a frozenset(). We pick the first in the set, which is
|
||||
# sorted alphabetically, thus we get 'bar.jpg', not 'foo.jpg', which
|
||||
# would probably make more sense.
|
||||
self.core.tracklist.add([Track(album=Album(images=[
|
||||
'http://example.com/foo.jpg', 'http://example.com/bar.jpg']))])
|
||||
self.core.playback.play()
|
||||
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
|
||||
self.assertIn('mpris:artUrl', result.keys())
|
||||
self.assertEqual(result['mpris:artUrl'], 'http://example.com/bar.jpg')
|
||||
|
||||
def test_get_metadata_has_no_art_url_if_no_album(self):
|
||||
self.core.tracklist.add([Track()])
|
||||
self.core.playback.play()
|
||||
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
|
||||
self.assertNotIn('mpris:artUrl', result.keys())
|
||||
|
||||
def test_get_metadata_has_no_art_url_if_no_album_images(self):
|
||||
self.core.tracklist.add([Track(Album(images=[]))])
|
||||
self.core.playback.play()
|
||||
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
|
||||
self.assertNotIn('mpris:artUrl', result.keys())
|
||||
|
||||
def test_get_metadata_has_disc_number_in_album(self):
|
||||
self.core.tracklist.add([Track(disc_no=2)])
|
||||
self.core.playback.play()
|
||||
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
|
||||
self.assertIn('xesam:discNumber', result.keys())
|
||||
self.assertEqual(result['xesam:discNumber'], 2)
|
||||
|
||||
def test_get_metadata_has_track_number_in_album(self):
|
||||
self.core.tracklist.add([Track(track_no=7)])
|
||||
self.core.playback.play()
|
||||
|
||||
@ -172,22 +172,29 @@ class ScannerTest(unittest.TestCase):
|
||||
self.check(
|
||||
'scanner/simple/song1.mp3', 'uri',
|
||||
'file://%s' % path_to_data_dir('scanner/simple/song1.mp3'))
|
||||
self.check(
|
||||
'scanner/simple/song1.ogg', 'uri',
|
||||
'file://%s' % path_to_data_dir('scanner/simple/song1.ogg'))
|
||||
|
||||
def test_duration_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
self.check('scanner/simple/song1.mp3', 'duration', 4680)
|
||||
self.check('scanner/simple/song1.ogg', 'duration', 4680)
|
||||
|
||||
def test_artist_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
self.check('scanner/simple/song1.mp3', 'artist', 'name')
|
||||
self.check('scanner/simple/song1.ogg', 'artist', 'name')
|
||||
|
||||
def test_album_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
self.check('scanner/simple/song1.mp3', 'album', 'albumname')
|
||||
self.check('scanner/simple/song1.ogg', 'album', 'albumname')
|
||||
|
||||
def test_track_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
self.check('scanner/simple/song1.mp3', 'title', 'trackname')
|
||||
self.check('scanner/simple/song1.ogg', 'title', 'trackname')
|
||||
|
||||
def test_nonexistant_folder_does_not_fail(self):
|
||||
self.scan('scanner/does-not-exist')
|
||||
|
||||
@ -48,6 +48,7 @@ class JsonRpcTestBase(unittest.TestCase):
|
||||
'core': self.core,
|
||||
'core.playback': self.core.playback,
|
||||
'core.tracklist': self.core.tracklist,
|
||||
'get_uri_schemes': self.core.get_uri_schemes,
|
||||
},
|
||||
encoders=[models.ModelJSONEncoder],
|
||||
decoders=[models.model_json_decoder])
|
||||
@ -188,6 +189,23 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
|
||||
|
||||
self.assertEqual(response['result'], None)
|
||||
|
||||
def test_call_method_which_is_a_directly_mounted_actor_member(self):
|
||||
# 'get_uri_schemes' isn't a regular callable, but a Pykka
|
||||
# CallableProxy. This test checks that CallableProxy objects are
|
||||
# threated by JsonRpcWrapper like any other callable.
|
||||
|
||||
request = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'get_uri_schemes',
|
||||
'id': 1,
|
||||
}
|
||||
response = self.jrw.handle_data(request)
|
||||
|
||||
self.assertEqual(response['jsonrpc'], '2.0')
|
||||
self.assertEqual(response['id'], 1)
|
||||
self.assertNotIn('error', response)
|
||||
self.assertEqual(response['result'], ['dummy'])
|
||||
|
||||
def test_call_method_with_positional_params(self):
|
||||
request = {
|
||||
'jsonrpc': '2.0',
|
||||
@ -588,6 +606,7 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
|
||||
|
||||
def test_inspector_can_describe_a_bunch_of_large_classes(self):
|
||||
inspector = jsonrpc.JsonRpcInspector({
|
||||
'core.get_uri_schemes': core.Core.get_uri_schemes,
|
||||
'core.library': core.LibraryController,
|
||||
'core.playback': core.PlaybackController,
|
||||
'core.playlists': core.PlaylistsController,
|
||||
@ -596,6 +615,9 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
|
||||
|
||||
methods = inspector.describe()
|
||||
|
||||
self.assertIn('core.get_uri_schemes', methods)
|
||||
self.assertEquals(len(methods['core.get_uri_schemes']['params']), 0)
|
||||
|
||||
self.assertIn('core.library.lookup', methods.keys())
|
||||
self.assertEquals(
|
||||
methods['core.library.lookup']['params'][0]['name'], 'uri')
|
||||
|
||||
@ -79,13 +79,13 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
result = setting_utils.validate_settings(
|
||||
self.defaults, {'FRONTENDS': []})
|
||||
self.assertEqual(
|
||||
result['FRONTENDS'], 'Must contain at least one value.')
|
||||
result['FRONTENDS'], 'Must be set.')
|
||||
|
||||
def test_empty_backends_list_returns_error(self):
|
||||
result = setting_utils.validate_settings(
|
||||
self.defaults, {'BACKENDS': []})
|
||||
self.assertEqual(
|
||||
result['BACKENDS'], 'Must contain at least one value.')
|
||||
result['BACKENDS'], 'Must be set.')
|
||||
|
||||
def test_noniterable_multivalue_setting_returns_error(self):
|
||||
result = setting_utils.validate_settings(
|
||||
|
||||
@ -35,5 +35,6 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.9.0'), SV('0.10.0'))
|
||||
self.assertLess(SV('0.10.0'), SV('0.11.0'))
|
||||
self.assertLess(SV('0.11.0'), SV('0.11.1'))
|
||||
self.assertLess(SV('0.11.1'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.12.1'))
|
||||
self.assertLess(SV('0.11.1'), SV('0.12.0'))
|
||||
self.assertLess(SV('0.12.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.13.1'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user