Merge branch 'develop' into feature/limit-search-by-uri-root

Conflicts:
	docs/changes.rst
This commit is contained in:
Stein Magnus Jodal 2013-03-31 12:45:31 +02:00
commit 06c7d8ea46
14 changed files with 406 additions and 161 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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