From 5e29647897505cdcf79596e7303b5f97171297a3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Mar 2013 23:53:29 +0100 Subject: [PATCH 01/10] js: Remove redundant config not working on Node 0.10 --- js/Gruntfile.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/js/Gruntfile.js b/js/Gruntfile.js index f290250a..195decd6 100644 --- a/js/Gruntfile.js +++ b/js/Gruntfile.js @@ -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 %>", From 74b4fdc7ee471ad9ac477c719fc6ea2dcc5a6019 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Mar 2013 13:51:28 +0100 Subject: [PATCH 02/10] js: Make test suite run on Node.js using faye-websocket --- js/buster.js | 10 +++++++++- js/package.json | 5 +++++ js/src/mopidy.js | 34 ++++++++++++++++++++++++++-------- js/test/mopidy-test.js | 35 ++++++++++++++++++++++++----------- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/js/buster.js b/js/buster.js index f789885a..37f41d8a 100644 --- a/js/buster.js +++ b/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"] +}; diff --git a/js/package.json b/js/package.json index 6638c705..f83c9273 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,11 @@ { "name": "mopidy", "version": "0.0.0", + "dependencies": { + "bane": "~0.4.0", + "faye-websocket": "~0.4.4", + "when": "~1.8.1" + }, "devDependencies": { "buster": "~0.6.12", "grunt": "~0.4.0", diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 5a75a836..011aec09 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -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; +} diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 8842ebf4..b694fd7e 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -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"}); From 461265f121415b556ba4cfb3cefa75e6eb6a5e49 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Mar 2013 01:09:34 +0100 Subject: [PATCH 03/10] docs: Add Node.js installation instructions --- mopidy/frontends/http/__init__.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index ab8dff42..3be4993e 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -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,6 +154,25 @@ 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. @@ -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 From 5e374350f5d3debd4e4a45dc215aa7ecdbf59125 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Mar 2013 01:18:31 +0100 Subject: [PATCH 04/10] js: Add more metadata to package.json for npm publishing --- js/README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ js/README.rst | 62 ------------------------------------- js/package.json | 14 ++++++++- 3 files changed, 95 insertions(+), 63 deletions(-) create mode 100644 js/README.md delete mode 100644 js/README.rst diff --git a/js/README.md b/js/README.md new file mode 100644 index 00000000..9601b64a --- /dev/null +++ b/js/README.md @@ -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 diff --git a/js/README.rst b/js/README.rst deleted file mode 100644 index e8782213..00000000 --- a/js/README.rst +++ /dev/null @@ -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 `_ 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 `_ targets which isn't predefined in -``package.json`` and thus isn't available through ``npm run-script``:: - - PATH=./node_modules/.bin:$PATH grunt foo diff --git a/js/package.json b/js/package.json index f83c9273..d3398ca0 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,18 @@ { "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", From 02ac0cae42be4217c61417b1b1fb80108d392d7d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Mar 2013 23:34:00 +0100 Subject: [PATCH 05/10] docs: Add Mopidy.js on Node to changelog --- docs/changes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index fd065b34..ef040b9e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -45,6 +45,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 `_ 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. From bfd2010639bd0879c3564516efd237e9ede1c227 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Mar 2013 23:31:17 +0100 Subject: [PATCH 06/10] core: Let tracklist.add() lookup tracks by URI --- docs/changes.rst | 6 ++++++ mopidy/core/tracklist.py | 14 +++++++++++++- tests/core/tracklist_test.py | 29 +++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index ef040b9e..e3664013 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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`) + **Audio sub-system** - Make audio error logging handle log messages with non-ASCII chars. (Fixes: diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 402e6c09..1c8f437f 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -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 diff --git a/tests/core/tracklist_test.py b/tests/core/tracklist_test.py index 550cfe63..93d914ed 100644 --- a/tests/core/tracklist_test.py +++ b/tests/core/tracklist_test.py @@ -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') From f26db23de9de2ce7b579e9b880b70ffa683330bb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Mar 2013 00:19:40 +0100 Subject: [PATCH 07/10] mpd: Use add(uri=uri) instead of add(lookup(uri)) --- mopidy/frontends/mpd/protocol/current_playlist.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index d1b0e59a..055d39e6 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -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[^"]*)"( "(?P\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) From 3d847862ae780d2e1958fbaed4bacc99201c2eff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Mar 2013 00:19:50 +0100 Subject: [PATCH 08/10] mpris: Use add(uri=uri) instead of add(lookup(uri)) --- mopidy/frontends/mpris/objects.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index a9a93b45..04a72676 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -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) From 0cc7d8f9bf31d70f1caaa5eeef3ad237c00adf50 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Mar 2013 03:03:10 +0200 Subject: [PATCH 09/10] docs: More on extensiondev --- docs/extensiondev.rst | 217 ++++++++++++++++++++++++++++++++---------- 1 file changed, 166 insertions(+), 51 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 05a62c2f..65747502 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -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 `_ -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``. From 0fe5ff8712d340f65f50d3ea6d85829e6e44a2e4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Mar 2013 07:51:21 +0200 Subject: [PATCH 10/10] docs: js/README.rst was renamed to .md --- mopidy/frontends/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 3be4993e..e81ddf3f 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -175,7 +175,7 @@ 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