Merge branch 'develop' into feature/limit-search-by-uri-root
Conflicts: docs/changes.rst
This commit is contained in:
commit
06c7d8ea46
@ -24,6 +24,12 @@ v0.13.0 (in development)
|
||||
the Mopidy process will now always make it log tracebacks for all alive
|
||||
threads.
|
||||
|
||||
- :meth:`mopidy.core.TracklistController.add` now accepts an ``uri`` which it
|
||||
will lookup in the libraries 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`
|
||||
@ -58,6 +64,11 @@ v0.13.0 (in development)
|
||||
|
||||
**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 1.8.1.
|
||||
|
||||
@ -49,8 +49,8 @@ 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
|
||||
Extension Registry. The license text should be included in the ``LICENSE`` file
|
||||
in the root of the extension's Git repo.
|
||||
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::
|
||||
@ -60,14 +60,21 @@ extension, Mopidy-Soundspot::
|
||||
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.
|
||||
|
||||
|
||||
README.rst
|
||||
----------
|
||||
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
|
||||
|
||||
@ -104,19 +111,41 @@ README.rst
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/develop#egg=mopidy-soundspot-dev>`_
|
||||
|
||||
|
||||
setup.py
|
||||
--------
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
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'),
|
||||
@ -138,11 +167,11 @@ setup.py
|
||||
'Mopidy',
|
||||
'pysoundspot',
|
||||
],
|
||||
entry_points=[
|
||||
entry_points={
|
||||
'mopidy.extension': [
|
||||
'mopidy_soundspot = mopidy_soundspot:EntryPoint',
|
||||
'soundspot = mopidy_soundspot:Extension',
|
||||
],
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
@ -154,32 +183,46 @@ setup.py
|
||||
)
|
||||
|
||||
|
||||
mopidy_soundspot/__init__.py
|
||||
----------------------------
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
import os
|
||||
|
||||
from mopidy.exceptions import ExtensionError
|
||||
from mopidy.utils import ext
|
||||
|
||||
|
||||
__version__ = '0.1'
|
||||
|
||||
|
||||
class EntryPoint(object):
|
||||
class Extension(ext.Extension):
|
||||
|
||||
name = 'Mopidy-Soundspot'
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
return """
|
||||
[soundspot]
|
||||
enabled = true
|
||||
username =
|
||||
password =
|
||||
"""
|
||||
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'):
|
||||
@ -189,32 +232,63 @@ mopidy_soundspot/__init__.py
|
||||
|
||||
@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 start_frontend(cls, core):
|
||||
def get_frontend_class(cls):
|
||||
from .frontend import SoundspotFrontend
|
||||
cls._frontend = SoundspotFrontend.start(core=core)
|
||||
return SoundspotFrontend
|
||||
|
||||
@classmethod
|
||||
def stop_frontend(cls):
|
||||
cls._frontend.stop()
|
||||
|
||||
@classmethod
|
||||
def start_backend(cls, audio):
|
||||
def get_backend_class(cls):
|
||||
from .backend import SoundspotBackend
|
||||
cls._backend = SoundspotBackend.start(audio=audio)
|
||||
return SoundspotBackend
|
||||
|
||||
@classmethod
|
||||
def stop_backend(cls):
|
||||
cls._backend.stop()
|
||||
def get_gstreamer_element_classes(cls):
|
||||
from .mixer import SoundspotMixer
|
||||
return [SoundspotMixer]
|
||||
|
||||
|
||||
mopidy_soundspot/frontend.py
|
||||
----------------------------
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
@ -222,6 +296,7 @@ mopidy_soundspot/frontend.py
|
||||
|
||||
from mopidy.core import CoreListener
|
||||
|
||||
|
||||
class SoundspotFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
super(SoundspotFrontend, self).__init__()
|
||||
@ -230,8 +305,15 @@ mopidy_soundspot/frontend.py
|
||||
# Your frontend implementation
|
||||
|
||||
|
||||
mopidy_soundspot/backend.py
|
||||
---------------------------
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
@ -239,6 +321,7 @@ mopidy_soundspot/backend.py
|
||||
|
||||
from mopidy.backends import base
|
||||
|
||||
|
||||
class SoundspotBackend(pykka.ThreadingActor, base.BaseBackend):
|
||||
def __init__(self, audio):
|
||||
super(SoundspotBackend, self).__init__()
|
||||
@ -247,35 +330,67 @@ mopidy_soundspot/backend.py
|
||||
# Your backend implementation
|
||||
|
||||
|
||||
Notes
|
||||
=====
|
||||
Example GStreamer element
|
||||
=========================
|
||||
|
||||
An extension wants to:
|
||||
If you want to extend Mopidy's GStreamer pipeline with new custom GStreamer
|
||||
elements, you'll need to get Mopidy to register them in GStreamer before they
|
||||
can be used.
|
||||
|
||||
- Be automatically found if installed
|
||||
- Either register a setuptools entry points on installation, or
|
||||
- Require a line of configuration to activate the extension
|
||||
Basically, you just implement your GStreamer element in Python and then make
|
||||
your :meth:`Extension.get_gstreamer_element_classes` method return a list with
|
||||
the classes of all your custom GStreamer elements.
|
||||
|
||||
- Provide default config
|
||||
For examples of custom GStreamer elements implemented in Python, see
|
||||
:mod:`mopidy.audio.mixers`.
|
||||
|
||||
- Validate configuration
|
||||
|
||||
- Pass all configuration to every extension, let the extension complain on
|
||||
anything it wants to
|
||||
Implementation steps
|
||||
====================
|
||||
|
||||
- Validate presence of dependencies
|
||||
A rough plan of how to make the above document the reality of how Mopidy
|
||||
extensions work.
|
||||
|
||||
- Python packages (e.g. pyspotify)
|
||||
1. Implement :class:`mopidy.utils.ext.Extension` base class and the
|
||||
:exc:`mopidy.exceptions.ExtensionError` exception class.
|
||||
|
||||
- Other software
|
||||
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`.
|
||||
|
||||
- The presence of other extensions can be validated in the configuration
|
||||
validation step
|
||||
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.
|
||||
|
||||
- Validate that needed TCP ports are free
|
||||
4. Add entry points for the existing extensions in the ``setup.py`` file.
|
||||
|
||||
- Register new GStreamer elements
|
||||
5. Rewrite the startup procedure to find extensions and thus frontends and
|
||||
backends via :mod:`pkg_resouces` instead of the ``FRONTENDS`` and
|
||||
``BACKENDS`` settings.
|
||||
|
||||
- Be asked to start running
|
||||
6. Remove the ``FRONTENDS`` and ``BACKENDS`` settings.
|
||||
|
||||
- Be asked to shut down
|
||||
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,
|
||||
|
||||
- ``/etc/mopidy.conf``,
|
||||
|
||||
- ``~/.config/mopidy.conf``, and
|
||||
|
||||
- any config file provided via command line arguments.
|
||||
|
||||
8. Add command line options for:
|
||||
|
||||
- printing the effective config,
|
||||
|
||||
- overriding a config temporarily,
|
||||
|
||||
- loading an additional config file, and
|
||||
|
||||
- write a config value permanently to ``~/.config/mopidy.conf``.
|
||||
|
||||
@ -15,11 +15,6 @@ module.exports = function (grunt) {
|
||||
minified: "../mopidy/frontends/http/data/mopidy.min.js"
|
||||
}
|
||||
},
|
||||
buster: {
|
||||
test: {
|
||||
config: "buster.js"
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
options: {
|
||||
banner: "<%= meta.banner %>",
|
||||
|
||||
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
|
||||
10
js/buster.js
10
js/buster.js
@ -1,9 +1,17 @@
|
||||
var config = module.exports;
|
||||
|
||||
config["tests"] = {
|
||||
config.browser_tests = {
|
||||
environment: "browser",
|
||||
libs: ["lib/**/*.js"],
|
||||
sources: ["src/**/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
};
|
||||
|
||||
config.node_tests = {
|
||||
environment: "node",
|
||||
libs: ["lib/**/*.js"],
|
||||
sources: ["src/**/*.js"],
|
||||
testHelpers: ["test/**/*-helper.js"],
|
||||
tests: ["test/**/*-test.js"]
|
||||
};
|
||||
|
||||
@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"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": "~1.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buster": "~0.6.12",
|
||||
"grunt": "~0.4.0",
|
||||
|
||||
@ -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"
|
||||
});
|
||||
@ -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"});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user