From 42981b94b60e4cc68de6d95c7d78dd551970ddab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 25 Nov 2012 22:16:36 +0100 Subject: [PATCH 01/17] tests: dict comp. is not supported in Python 2.6 --- tests/utils/jsonrpc_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index 64b5e628..c3c4b1a0 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -260,7 +260,7 @@ class JsonRpcBatchTest(JsonRpcTestBase): self.assertEqual(len(response), 3) - response = {row['id']: row for row in response} + response = dict((row['id'], row) for row in response) self.assertEqual(response[1]['result'], False) self.assertEqual(response[2]['result'], True) self.assertEqual(response[3]['result'], False) From 961fce13e52790dacb43cbd40063b7f1ee4c468f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 25 Nov 2012 23:04:09 +0100 Subject: [PATCH 02/17] tests: Make it work on Python 2.6 (try 2) --- tests/utils/jsonrpc_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index c3c4b1a0..cacc7341 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -277,7 +277,7 @@ class JsonRpcBatchTest(JsonRpcTestBase): self.assertEqual(len(response), 2) - response = {row['id']: row for row in response} + response = dict((row['id'], row) for row in response) self.assertNotIn(1, response) self.assertEqual(response[2]['result'], True) self.assertEqual(response[3]['result'], False) @@ -522,7 +522,7 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase): response = self.jrw.handle_data(request) self.assertEqual(len(response), 5) - response = {row['id']: row for row in response} + response = dict((row['id'], row) for row in response) self.assertEqual(response['1']['result'], None) self.assertEqual(response['2']['result'], None) self.assertEqual(response[None]['error']['code'], -32600) From 69ede85959c6d70b155c2804da31153854d76714 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 25 Nov 2012 23:24:04 +0100 Subject: [PATCH 03/17] tests: Exception messages on 2.6 and 2.7 differs --- tests/utils/jsonrpc_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index cacc7341..7c8a0a9b 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -313,7 +313,7 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase): data = error['data'] self.assertEqual(data['type'], 'ValueError') - self.assertEqual(data['message'], "u'bogus' is not in list") + self.assertIn('not in list', data['message']) self.assertIn('traceback', data) self.assertIn('Traceback (most recent call last):', data['traceback']) From 6a0e9aa69c2e8eb19c8d3dc2cc4355831048403a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Nov 2012 15:22:31 +0100 Subject: [PATCH 04/17] spotify: Playlist refresh hack should not be active after first run --- mopidy/backends/spotify/session_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index cfe4e433..2336ad4d 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -142,8 +142,9 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): # startup until the Spotify backend is ready from 35s to 12s in one # test with clean Spotify cache. In cases with an outdated cache # the time improvements should be a lot greater. - self._initial_data_receive_completed = True - self.refresh_playlists() + if not self._initial_data_receive_completed: + self._initial_data_receive_completed = True + self.refresh_playlists() def end_of_track(self, session): """Callback used by pyspotify""" From 0b9ee92edbabf4410605fd97ef29e42f43d421d3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Nov 2012 21:14:44 +0100 Subject: [PATCH 05/17] local: Change log level from error to warning --- mopidy/backends/local/playlists.py | 2 +- mopidy/backends/local/translator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index 666532c5..53f7aaae 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -57,7 +57,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): # from other backends tracks += self.backend.library.lookup(track_uri) except LookupError as ex: - logger.error('Playlist item could not be added: %s', ex) + logger.warning('Playlist item could not be added: %s', ex) playlist = Playlist(uri=uri, name=name, tracks=tracks) playlists.append(playlist) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 21e389ea..59e2957a 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -35,7 +35,7 @@ def parse_m3u(file_path, music_folder): with open(file_path) as m3u: contents = m3u.readlines() except IOError as error: - logger.error('Couldn\'t open m3u: %s', locale_decode(error)) + logger.warning('Couldn\'t open m3u: %s', locale_decode(error)) return uris for line in contents: @@ -64,7 +64,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''): with open(tag_cache) as library: contents = library.read() except IOError as error: - logger.error('Could not open tag cache: %s', locale_decode(error)) + logger.warning('Could not open tag cache: %s', locale_decode(error)) return tracks current = {} From 44ad69d7c4486b0dd5f64b62757193fe8fd66c5f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 02:27:01 +0100 Subject: [PATCH 06/17] core: Add missing getter method --- mopidy/core/playback.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index e50de2e7..4941ef0f 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -53,6 +53,9 @@ class PlaybackController(object): Tracks are not removed from the playlist. """ + def get_current_tl_track(self): + return self.current_tl_track + current_tl_track = None """ The currently playing or selected :class:`mopidy.models.TlTrack`, or From 0423d5289b0134f42d1b4c1cadc8be1d4ef95efe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 18:26:25 +0100 Subject: [PATCH 07/17] http: Mark security and API stability sections as warnings --- mopidy/frontends/http/__init__.py | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index fd1d2b01..44096f6f 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -20,14 +20,16 @@ Frontend which lets you control Mopidy through HTTP and WebSockets. When this frontend is included in :attr:`mopidy.settings.FRONTENDS`, it starts a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`. -As a simple security measure, the web server is by default only available from -localhost. To make it available from other computers, change -:attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that the -HTTP frontend does not feature any form of user authentication or -authorization. Anyone able to access the web server can use the full core API -of Mopidy. Thus, you probably only want to make the web server available from -your local network or place it behind a web proxy which takes care or user -authentication. You have been warned. +.. warning:: Security + + As a simple security measure, the web server is by default only available + from localhost. To make it available from other computers, change + :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that + the HTTP frontend does not feature any form of user authentication or + authorization. Anyone able to access the web server can use the full core + API of Mopidy. Thus, you probably only want to make the web server + available from your local network or place it behind a web proxy which + takes care or user authentication. You have been warned. This web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you access to Mopidy's full API and enables Mopidy to instantly push events to the @@ -40,6 +42,18 @@ directory you want to serve. **WebSocket API** +.. warning:: API stability + + Since this frontend exposes our internal core API directly it is to be + regarded as **experimental**. We cannot promise to keep any form of + backwards compatibility between releases as we will need to change the core + API while working out how to support new use cases. Thus, if you use this + API, you must expect to do small adjustments to your client for every + release of Mopidy. + + From Mopidy 1.0 and onwards, we intend to keep the core API far more + stable. + On the WebSocket we send two different kind of messages: The client can send JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses. In addition, the server will send event messages when something happens on the @@ -84,16 +98,6 @@ look at the ``core.describe`` response can be helpful. A JavaScript library wrapping the JSON-RPC over WebSocket API is under development. Details on it will appear here when it's released. - -**API stability** - -Since this frontend exposes our internal core API directly it is to be regarded -as **experimental**. We cannot promise to keep any form of backwards -compatibility between releases as we will need to change the core API while -working out how to support new use cases. Thus, if you use this API, you must -expect to do small adjustments to your client for every release of Mopidy. - -From Mopidy 1.0 and onwards, we intend to keep the core API far more stable. """ # flake8: noqa From 90859c903b4ff5b268e5bd177de98065f6799ccb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 21:57:12 +0100 Subject: [PATCH 08/17] http: Add favicon --- mopidy/frontends/http/actor.py | 5 +++++ mopidy/frontends/http/data/favicon.png | Bin 0 -> 5997 bytes 2 files changed, 5 insertions(+) create mode 100644 mopidy/frontends/http/data/favicon.png diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 65cf9445..ef6808f0 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -55,6 +55,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): logger.debug('HTTP server will serve "%s" at /', static_dir) mopidy_dir = os.path.join(os.path.dirname(__file__), 'data') + favicon = os.path.join(mopidy_dir, 'favicon.png') config = { b'/': { @@ -62,6 +63,10 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): 'tools.staticdir.index': 'index.html', 'tools.staticdir.dir': static_dir, }, + b'/favicon.ico': { + 'tools.staticfile.on': True, + 'tools.staticfile.filename': favicon, + }, b'/mopidy': { 'tools.staticdir.on': True, 'tools.staticdir.index': 'mopidy.html', diff --git a/mopidy/frontends/http/data/favicon.png b/mopidy/frontends/http/data/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..a214c91fb4fa045dfbb5ea7d5414eda99d6d4dbe GIT binary patch literal 5997 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 literal 0 HcmV?d00001 From 6238f55ae20a29fea8cf99af05332ab3c2166bc2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 22:39:34 +0100 Subject: [PATCH 09/17] core: Add CoreListener.on_event() The `on_event()` method is called on all events. By default, it forwards the event to the specific event handler methods. It's also a convenient method to override if you want to handle all events in one place. --- mopidy/core/listener.py | 15 ++++++++++++++- tests/core/listener_test.py | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mopidy/core/listener.py b/mopidy/core/listener.py index dc8bf1d7..7c4ab093 100644 --- a/mopidy/core/listener.py +++ b/mopidy/core/listener.py @@ -19,7 +19,20 @@ class CoreListener(object): """Helper to allow calling of core listener events""" listeners = pykka.ActorRegistry.get_by_class(CoreListener) for listener in listeners: - getattr(listener.proxy(), event)(**kwargs) + listener.proxy().on_event(event, **kwargs) + + def on_event(self, event, **kwargs): + """ + Called on all events. + + *MAY* be implemented by actor. By default, this method forwards the + event to the specific event methods. + + :param event: the event name + :type event: string + :param kwargs: any other arguments to the specific event handlers + """ + getattr(self, event)(**kwargs) def track_playback_paused(self, track, time_position): """ diff --git a/tests/core/listener_test.py b/tests/core/listener_test.py index 2e121796..8aaf1234 100644 --- a/tests/core/listener_test.py +++ b/tests/core/listener_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import mock + from mopidy.core import CoreListener, PlaybackState from mopidy.models import Playlist, Track @@ -10,6 +12,15 @@ class CoreListenerTest(unittest.TestCase): def setUp(self): self.listener = CoreListener() + def test_on_event_forwards_to_specific_handler(self): + self.listener.track_playback_paused = mock.Mock() + + self.listener.on_event( + 'track_playback_paused', track=Track(), position=0) + + self.listener.track_playback_paused.assert_called_with( + track=Track(), position=0) + def test_listener_has_default_impl_for_track_playback_paused(self): self.listener.track_playback_paused(Track(), 0) From 2edc884e76fcb7a4bd7d46f73aee96c9c86da16f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 22:41:13 +0100 Subject: [PATCH 10/17] http: Override CoreListener.on_event() instead of the specific event handlers --- mopidy/frontends/http/actor.py | 35 +---------- tests/frontends/http/events_test.py | 94 +---------------------------- 2 files changed, 3 insertions(+), 126 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index ef6808f0..34f39a4c 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -98,40 +98,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): cherrypy.engine.exit() logger.info('Stopped HTTP server') - def track_playback_paused(self, **data): - self._broadcast_event('track_playback_paused', data) - - def track_playback_resumed(self, **data): - self._broadcast_event('track_playback_resumed', data) - - def track_playback_started(self, **data): - self._broadcast_event('track_playback_started', data) - - def track_playback_ended(self, **data): - self._broadcast_event('track_playback_ended', data) - - def playback_state_changed(self, **data): - self._broadcast_event('playback_state_changed', data) - - def tracklist_changed(self, **data): - self._broadcast_event('tracklist_changed', data) - - def playlists_loaded(self, **data): - self._broadcast_event('playlists_loaded', data) - - def playlist_changed(self, **data): - self._broadcast_event('playlist_changed', data) - - def options_changed(self, **data): - self._broadcast_event('options_changed', data) - - def volume_changed(self, **data): - self._broadcast_event('volume_changed', data) - - def seeked(self, **data): - self._broadcast_event('seeked', data) - - def _broadcast_event(self, name, data): + def on_event(self, name, **data): event = {} event.update(data) event['event'] = name diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index 9df4a2b5..d04eb93e 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -15,7 +15,7 @@ class HttpEventsTest(unittest.TestCase): def test_track_playback_paused_is_broadcasted(self, publish): publish.reset_mock() - self.http.track_playback_paused(foo='bar') + self.http.on_event('track_playback_paused', foo='bar') self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') self.assertDictEqual( json.loads(str(publish.call_args[0][1])), { @@ -25,100 +25,10 @@ class HttpEventsTest(unittest.TestCase): def test_track_playback_resumed_is_broadcasted(self, publish): publish.reset_mock() - self.http.track_playback_resumed(foo='bar') + self.http.on_event('track_playback_resumed', foo='bar') self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') self.assertDictEqual( json.loads(str(publish.call_args[0][1])), { 'event': 'track_playback_resumed', 'foo': 'bar', }) - - def test_track_playback_started_is_broadcasted(self, publish): - publish.reset_mock() - self.http.track_playback_started(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'track_playback_started', - 'foo': 'bar', - }) - - def test_track_playback_ended_is_broadcasted(self, publish): - publish.reset_mock() - self.http.track_playback_ended(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'track_playback_ended', - 'foo': 'bar', - }) - - def test_playback_state_changed_is_broadcasted(self, publish): - publish.reset_mock() - self.http.playback_state_changed(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'playback_state_changed', - 'foo': 'bar', - }) - - def test_tracklist_changed_is_broadcasted(self, publish): - publish.reset_mock() - self.http.tracklist_changed(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'tracklist_changed', - 'foo': 'bar', - }) - - def test_playlists_loaded_is_broadcasted(self, publish): - publish.reset_mock() - self.http.playlists_loaded(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'playlists_loaded', - 'foo': 'bar', - }) - - def test_playlist_changed_is_broadcasted(self, publish): - publish.reset_mock() - self.http.playlist_changed(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'playlist_changed', - 'foo': 'bar', - }) - - def test_options_changed_is_broadcasted(self, publish): - publish.reset_mock() - self.http.options_changed(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'options_changed', - 'foo': 'bar', - }) - - def test_volume_changed_is_broadcasted(self, publish): - publish.reset_mock() - self.http.volume_changed(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'volume_changed', - 'foo': 'bar', - }) - - def test_seeked_is_broadcasted(self, publish): - publish.reset_mock() - self.http.seeked(foo='bar') - self.assertEqual(publish.call_args[0][0], 'websocket-broadcast') - self.assertDictEqual( - json.loads(str(publish.call_args[0][1])), { - 'event': 'seeked', - 'foo': 'bar', - }) From d6a906a723cf045bcb040e556460724dc11e8f98 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 23:15:13 +0100 Subject: [PATCH 11/17] http: No need to copy dict --- mopidy/frontends/http/actor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 34f39a4c..8ad0f026 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -99,8 +99,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): logger.info('Stopped HTTP server') def on_event(self, name, **data): - event = {} - event.update(data) + event = data event['event'] = name message = json.dumps(event, cls=models.ModelJSONEncoder) cherrypy.engine.publish('websocket-broadcast', TextMessage(message)) From 430d604509e2e45bbc08bd9883901c6b2180c1ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 30 Nov 2012 23:50:55 +0100 Subject: [PATCH 12/17] http: Revised the HTTP frontend docs --- mopidy/frontends/http/__init__.py | 48 ++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 44096f6f..d98734b2 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -1,5 +1,6 @@ """ -Frontend which lets you control Mopidy through HTTP and WebSockets. +The HTTP frontends lets you control Mopidy through HTTP and WebSockets, e.g. +from a web based client. **Dependencies** @@ -15,7 +16,9 @@ Frontend which lets you control Mopidy through HTTP and WebSockets. - :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` -**Usage** + +Setup +===== When this frontend is included in :attr:`mopidy.settings.FRONTENDS`, it starts a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`. @@ -31,16 +34,28 @@ a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`. available from your local network or place it behind a web proxy which takes care or user authentication. You have been warned. -This web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you -access to Mopidy's full API and enables Mopidy to instantly push events to the -client, as they happen. + +Using a web based Mopidy client +=============================== The web server can also host any static files, for example the HTML, CSS, -JavaScript and images needed by a web based Mopidy client. To host static +JavaScript, and images needed for a web based Mopidy client. To host static files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the -directory you want to serve. +root directory of your web client, e.g.:: -**WebSocket API** + HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client' + +If the directory includes a file named ``index.html``, it will be served on the +root of Mopidy's web server. + +If you're making a web based client and wants to do server side development as +well, you are of course free to run your own web server and just use Mopidy's +web server for the APIs. But, for clients implemented purely in JavaScript, +letting Mopidy host the files is a simpler solution. + + +WebSocket API +============= .. warning:: API stability @@ -54,11 +69,19 @@ directory you want to serve. From Mopidy 1.0 and onwards, we intend to keep the core API far more stable. +The web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you +access to Mopidy's full API and enables Mopidy to instantly push events to the +client, as they happen. + On the WebSocket we send two different kind of messages: The client can send JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses. In addition, the server will send event messages when something happens on the server. Both message types are encoded as JSON objects. + +Event messages +-------------- + Event objects will always have a key named ``event`` whose value is the event type. Depending on the event type, the event may include additional fields for related data. The events maps directly to the :class:`mopidy.core.CoreListener` @@ -68,6 +91,10 @@ fields on the event objects. Example event message:: {"event": "track_playback_started", "track": {...}} + +JSON-RPC 2.0 messaging +---------------------- + JSON-RPC 2.0 messages can be recognized by checking for the key named ``jsonrpc`` with the string value ``2.0``. For details on the messaging format, please refer to the `JSON-RPC 2.0 spec @@ -80,7 +107,7 @@ JSON-RPC calls over the WebSocket. For example, The core API's attributes is made available through setters and getters. For example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is -availableas the JSON-RPC method ``core.playback.get_current_track`. +available as the JSON-RPC method ``core.playback.get_current_track``. Example JSON-RPC request:: @@ -94,7 +121,8 @@ The JSON-RPC method ``core.describe`` returns a data structure describing all available methods. If you're unsure how the core API maps to JSON-RPC, having a look at the ``core.describe`` response can be helpful. -**JavaScript wrapper** +JavaScript wrapper +================== A JavaScript library wrapping the JSON-RPC over WebSocket API is under development. Details on it will appear here when it's released. From 9ec53bb3b7104c72bb6d6d17c7c22519ba900a39 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 1 Dec 2012 09:56:28 +0100 Subject: [PATCH 13/17] utils: Include cherrypy in --list-deps --- mopidy/utils/deps.py | 12 ++++++++++++ tests/utils/deps_test.py | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py index 3c177036..480dc180 100644 --- a/mopidy/utils/deps.py +++ b/mopidy/utils/deps.py @@ -35,6 +35,7 @@ def format_dependency_list(adapters=None): pylast_info, dbus_info, serial_info, + cherrypy_info, ] lines = [] @@ -189,3 +190,14 @@ def serial_info(): except ImportError: pass return dep_info + + +def cherrypy_info(): + dep_info = {'name': 'cherrypy'} + try: + import cherrypy + dep_info['version'] = cherrypy.__version__ + dep_info['path'] = cherrypy.__file__ + except ImportError: + pass + return dep_info diff --git a/tests/utils/deps_test.py b/tests/utils/deps_test.py index 168f98e5..d301cc91 100644 --- a/tests/utils/deps_test.py +++ b/tests/utils/deps_test.py @@ -27,6 +27,11 @@ try: except ImportError: spotify = False +try: + import cherrypy +except ImportError: + cherrypy = False + from mopidy.utils import deps from tests import unittest @@ -115,3 +120,11 @@ class DepsTest(unittest.TestCase): self.assertEquals('pyserial', result['name']) self.assertEquals(serial.VERSION, result['version']) self.assertIn('serial', result['path']) + + @unittest.skipUnless(cherrypy, 'cherrypy not found') + def test_cherrypy_info(self): + result = deps.cherrypy_info() + + self.assertEquals('cherrypy', result['name']) + self.assertEquals(cherrypy.__version__, result['version']) + self.assertIn('cherrypy', result['path']) From 5422d85f5b3b91572361181f60688ac0c0ade58e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 1 Dec 2012 09:58:21 +0100 Subject: [PATCH 14/17] utils: Include ws4py in --list-deps --- mopidy/utils/deps.py | 12 ++++++++++++ tests/utils/deps_test.py | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py index 480dc180..c83780fb 100644 --- a/mopidy/utils/deps.py +++ b/mopidy/utils/deps.py @@ -36,6 +36,7 @@ def format_dependency_list(adapters=None): dbus_info, serial_info, cherrypy_info, + ws4py_info, ] lines = [] @@ -201,3 +202,14 @@ def cherrypy_info(): except ImportError: pass return dep_info + + +def ws4py_info(): + dep_info = {'name': 'ws4py'} + try: + import ws4py + dep_info['version'] = ws4py.__version__ + dep_info['path'] = ws4py.__file__ + except ImportError: + pass + return dep_info diff --git a/tests/utils/deps_test.py b/tests/utils/deps_test.py index d301cc91..65a1eda1 100644 --- a/tests/utils/deps_test.py +++ b/tests/utils/deps_test.py @@ -32,6 +32,11 @@ try: except ImportError: cherrypy = False +try: + import ws4py +except ImportError: + ws4py = False + from mopidy.utils import deps from tests import unittest @@ -128,3 +133,11 @@ class DepsTest(unittest.TestCase): self.assertEquals('cherrypy', result['name']) self.assertEquals(cherrypy.__version__, result['version']) self.assertIn('cherrypy', result['path']) + + @unittest.skipUnless(ws4py, 'ws4py not found') + def test_ws4py_info(self): + result = deps.ws4py_info() + + self.assertEquals('ws4py', result['name']) + self.assertEquals(ws4py.__version__, result['version']) + self.assertIn('ws4py', result['path']) From ec66cae7843a459e093cd60a5aa23e6888b98a9f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 1 Dec 2012 10:48:54 +0100 Subject: [PATCH 15/17] tests: Ignore http tests if cherrypy is missing --- tests/frontends/http/events_test.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index d04eb93e..631802c4 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -1,14 +1,22 @@ import json -import cherrypy +try: + import cherrypy +except ImportError: + cherrypy = False import mock -from mopidy.frontends.http import HttpFrontend +from mopidy.exceptions import OptionalDependencyError +try: + from mopidy.frontends.http import HttpFrontend +except OptionalDependencyError: + pass from tests import unittest -@mock.patch.object(cherrypy.engine, 'publish') +@unittest.skipUnless(cherrypy, 'cherrypy not found') +@mock.patch('cherrypy.engine.publish') class HttpEventsTest(unittest.TestCase): def setUp(self): self.http = HttpFrontend(core=mock.Mock()) From 959bd6cd841fccbed68e619ee4fd60293f31e670 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 1 Dec 2012 11:20:44 +0100 Subject: [PATCH 16/17] audio: Add AudioListener.on_event() --- mopidy/audio/listener.py | 15 ++++++++++++++- tests/audio/listener_test.py | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/listener.py b/mopidy/audio/listener.py index da5f7b39..f8fedc67 100644 --- a/mopidy/audio/listener.py +++ b/mopidy/audio/listener.py @@ -19,7 +19,20 @@ class AudioListener(object): """Helper to allow calling of audio listener events""" listeners = pykka.ActorRegistry.get_by_class(AudioListener) for listener in listeners: - getattr(listener.proxy(), event)(**kwargs) + listener.proxy().on_event(event, **kwargs) + + def on_event(self, event, **kwargs): + """ + Called on all events. + + *MAY* be implemented by actor. By default, this method forwards the + event to the specific event methods. + + :param event: the event name + :type event: string + :param kwargs: any other arguments to the specific event handlers + """ + getattr(self, event)(**kwargs) def reached_end_of_stream(self): """ diff --git a/tests/audio/listener_test.py b/tests/audio/listener_test.py index b3274721..2c6da8f4 100644 --- a/tests/audio/listener_test.py +++ b/tests/audio/listener_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import mock + from mopidy import audio from tests import unittest @@ -9,6 +11,15 @@ class AudioListenerTest(unittest.TestCase): def setUp(self): self.listener = audio.AudioListener() + def test_on_event_forwards_to_specific_handler(self): + self.listener.state_changed = mock.Mock() + + self.listener.on_event( + 'state_changed', old_state='stopped', new_state='playing') + + self.listener.state_changed.assert_called_with( + old_state='stopped', new_state='playing') + def test_listener_has_default_impl_for_reached_end_of_stream(self): self.listener.reached_end_of_stream() From ac6cecd2f87f543de23da03e8e6af326de859045 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 1 Dec 2012 11:21:06 +0100 Subject: [PATCH 17/17] backends: Add BackendListener.on_event() --- mopidy/backends/listener.py | 15 ++++++++++++++- tests/backends/listener_test.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/listener.py b/mopidy/backends/listener.py index 30b3291d..d9043079 100644 --- a/mopidy/backends/listener.py +++ b/mopidy/backends/listener.py @@ -21,7 +21,20 @@ class BackendListener(object): """Helper to allow calling of backend listener events""" listeners = pykka.ActorRegistry.get_by_class(BackendListener) for listener in listeners: - getattr(listener.proxy(), event)(**kwargs) + listener.proxy().on_event(event, **kwargs) + + def on_event(self, event, **kwargs): + """ + Called on all events. + + *MAY* be implemented by actor. By default, this method forwards the + event to the specific event methods. + + :param event: the event name + :type event: string + :param kwargs: any other arguments to the specific event handlers + """ + getattr(self, event)(**kwargs) def playlists_loaded(self): """ diff --git a/tests/backends/listener_test.py b/tests/backends/listener_test.py index a4df513c..4aee451e 100644 --- a/tests/backends/listener_test.py +++ b/tests/backends/listener_test.py @@ -1,13 +1,22 @@ from __future__ import unicode_literals +import mock + from mopidy.backends.listener import BackendListener from tests import unittest -class CoreListenerTest(unittest.TestCase): +class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() + def test_on_event_forwards_to_specific_handler(self): + self.listener.playlists_loaded = mock.Mock() + + self.listener.on_event('playlists_loaded') + + self.listener.playlists_loaded.assert_called_with() + def test_listener_has_default_impl_for_playlists_loaded(self): self.listener.playlists_loaded()