From d166e9fd98e15799e3e8fba0e4e600f5831efba9 Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 25 Oct 2015 22:16:47 +0200 Subject: [PATCH 01/25] Resume playback after rating is applied, expand auto_setup routine. --- README.rst | 16 ++++-- mopidy_pandora/__init__.py | 2 +- mopidy_pandora/doubleclick.py | 27 ++++++--- mopidy_pandora/playback.py | 38 ++++++++++--- mopidy_pandora/rpc.py | 4 ++ tests/test_doubleclick.py | 67 ++++++++++++---------- tests/test_playback.py | 104 +++++++++++++++++++++++++++------- 7 files changed, 187 insertions(+), 71 deletions(-) diff --git a/README.rst b/README.rst index d0aee42..cc55bb3 100644 --- a/README.rst +++ b/README.rst @@ -74,9 +74,9 @@ alphabetical order. and web extensions: - double_click_interval - successive button clicks that occur within this interval (in seconds) will trigger the event. -- on_pause_resume_click - click pause and then play while a song is playing to trigger the event. Song continues afterwards. -- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :( -- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :( +- on_pause_resume_click - click pause and then play while a song is playing to trigger the event +- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. +- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. The supported events are: thumbs_up, thumbs_down, sleep, add_artist_bookmark, add_song_bookmark @@ -86,8 +86,7 @@ Usage Mopidy needs `dynamic playlist `_ and `core extensions `_ support to properly support Pandora. In the meantime, Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode**, -and Mopidy-Pandora will enable this automatically just before each track is changed unless you set the **auto_set_repeat** -config parameter to 'false'. +and Mopidy-Pandora will enable this automatically unless you set the **auto_set_repeat** config parameter to 'false'. Each time a track is played, the next dynamic track for that Pandora station will be played. The playlist will consist of a single track unless the experimental ratings support is enabled. With ratings support enabled, the playlist will @@ -107,10 +106,15 @@ Project resources Changelog ========= +v0.1.6 (UNRELEASED) +---------------------------------------- + +- Fix to resume playback after a track has been rated. + v0.1.5 (UNRELEASED) ---------------------------------------- -- Add option to automatically set tracks to play in repeat mode, using Mopidy's 'about-to-finish' callback. +- Add option to automatically set tracks to play in repeat mode when Mopidy-Pandora starts. - Add experimental support for rating songs by re-using buttons available in the current front-end Mopidy extensions. - Audio quality now defaults to the highest setting. - Improved caching to revert to Pandora server if station cannot be found in the local cache. diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index fbc27b7..2753220 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -8,7 +8,7 @@ from pandora import BaseAPIClient -__version__ = '0.1.5' +__version__ = '0.1.6' logger = logging.getLogger(__name__) diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index 80df722..5fc1a9a 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -14,13 +14,26 @@ def __init__(self, config, client): self.on_pause_previous_click = config["on_pause_previous_click"] self.double_click_interval = config['double_click_interval'] self.client = client - self.click_time = 0 + self._click_time = 0 - def set_click(self): - self.click_time = time.time() + def set_click_time(self, click_time=None): + if click_time is None: + self._click_time = time.time() + else: + self._click_time = click_time - def is_double_click(self): - return time.time() - self.click_time < float(self.double_click_interval) + def get_click_time(self): + return self._click_time + + def is_double_click(self, time_position=None): + + if self._click_time == 0: + return False + + if time_position is None: + time_position = 1 + + return time.time() - self._click_time < float(self.double_click_interval) and time_position > 0 def on_change_track(self, active_track_uri, new_track_uri): from mopidy_pandora.uri import PandoraUri @@ -41,17 +54,17 @@ def on_change_track(self, active_track_uri, new_track_uri): self.process_click(self.on_pause_previous_click, active_track_uri) def on_resume_click(self, track_uri, time_position): - if not self.is_double_click() or time_position == 0: + if not self.is_double_click(time_position): return self.process_click(self.on_pause_resume_click, track_uri) def process_click(self, method, track_uri): + uri = PandoraUri.parse(track_uri) logger.info("Triggering event '%s' for song: %s", method, uri.name) func = getattr(self, method) func(uri.token) - self.click_time = 0 def thumbs_up(self, track_token): self.client.add_feedback(track_token, True) diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index 844fef5..db10ff3 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -1,3 +1,5 @@ +from threading import Thread + from mopidy import backend, models from mopidy.internal import encoding @@ -17,19 +19,28 @@ def __init__(self, audio, backend): self._station_iter = None self.active_track_uri = None - if self.backend.auto_set_repeat: - # Make sure that tracks are being played in 'repeat mode'. - self.audio.set_about_to_finish_callback(self.callback).get() - - def callback(self): # TODO: add gapless playback when it is supported in Mopidy > 1.1 + # self.audio.set_about_to_finish_callback(self.callback).get() + + # def callback(self): # See: https://discuss.mopidy.com/t/has-the-gapless-playback-implementation-been-completed-yet/784/2 # self.audio.set_uri(self.translate_uri(self.get_next_track())).get() + def _auto_set_repeat(self): + uri = self.backend.rpc_client.get_current_track_uri() + + # Make sure that tracks are being played in 'repeat mode'. if uri is not None and uri.startswith("pandora:"): self.backend.rpc_client.set_repeat() + def prepare_change(self): + + if self.backend.auto_set_repeat: + Thread(target=self._auto_set_repeat).start() + + super(PandoraPlaybackProvider, self).prepare_change() + def change_track(self, track): if track.uri is None: @@ -85,12 +96,25 @@ def __init__(self, audio, backend): def change_track(self, track): self._double_click_handler.on_change_track(self.active_track_uri, track.uri) - return super(EventSupportPlaybackProvider, self).change_track(track) + return_value = super(EventSupportPlaybackProvider, self).change_track(track) + + if self._double_click_handler.get_click_time() > 0: + self._double_click_handler.set_click_time(0) + Thread(target=self.backend.rpc_client.resume_playback).start() + + return return_value def pause(self): - self._double_click_handler.set_click() + + if self.get_time_position() > 0: + self._double_click_handler.set_click_time() + return super(EventSupportPlaybackProvider, self).pause() def resume(self): self._double_click_handler.on_resume_click(self.active_track_uri, self.get_time_position()) + + if self._double_click_handler.get_click_time() > 0: + self._double_click_handler.set_click_time(0) + return super(EventSupportPlaybackProvider, self).resume() diff --git a/mopidy_pandora/rpc.py b/mopidy_pandora/rpc.py index cc62e11..19ebcb7 100644 --- a/mopidy_pandora/rpc.py +++ b/mopidy_pandora/rpc.py @@ -26,3 +26,7 @@ def get_current_track_uri(self): response = self._do_rpc('core.playback.get_current_tl_track') return response.json()['result']['track']['uri'] + + def resume_playback(self): + + self._do_rpc('core.playback.resume') diff --git a/tests/test_doubleclick.py b/tests/test_doubleclick.py index ac27d25..2323b9e 100644 --- a/tests/test_doubleclick.py +++ b/tests/test_doubleclick.py @@ -11,6 +11,7 @@ from mopidy_pandora.backend import MopidyPandoraAPIClient from mopidy_pandora.doubleclick import DoubleClickHandler +from mopidy_pandora.playback import PandoraPlaybackProvider from mopidy_pandora.uri import PandoraUri, TrackUri @@ -29,7 +30,8 @@ def handler(config): sleep_mock = mock.PropertyMock() handler.client.sleep_song = sleep_mock - handler.set_click() + handler.set_click_time() + return handler @@ -37,24 +39,10 @@ def test_is_double_click(handler): assert handler.is_double_click() - time.sleep(float(handler.double_click_interval) + 0.1) - assert handler.is_double_click() is False - - -def test_no_duplicate_triggers(handler, playlist_item_mock): - - assert handler.is_double_click() - - thumbs_up_mock = mock.PropertyMock() - handler.thumbs_up = thumbs_up_mock - - track_uri = TrackUri.from_track(playlist_item_mock).uri - handler.on_resume_click(track_uri, 100) + assert handler.is_double_click(0) is False + time.sleep(float(handler.double_click_interval) + 0.1) assert handler.is_double_click() is False - handler.on_resume_click(track_uri, 100) - - handler.thumbs_up.assert_called_once_with(PandoraUri.parse(track_uri).token) def test_on_change_track_forward(config, handler, playlist_item_mock): @@ -101,35 +89,56 @@ def test_on_resume_click_ignored_if_start_of_track(handler, playlist_item_mock): def test_on_resume_click(config, handler, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - process_click_mock = mock.PropertyMock() - handler.process_click = process_click_mock + process_click_mock = mock.PropertyMock() + handler.process_click = process_click_mock - track_uri = TrackUri.from_track(playlist_item_mock).uri - handler.on_resume_click(track_uri, 100) + track_uri = TrackUri.from_track(playlist_item_mock).uri + handler.on_resume_click(track_uri, 100) - handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], track_uri) + handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], track_uri) -def test_process_click(config, handler, playlist_item_mock): +def test_process_click_resume(config, handler, playlist_item_mock): thumbs_up_mock = mock.PropertyMock() - thumbs_down_mock = mock.PropertyMock() - sleep_mock = mock.PropertyMock() handler.thumbs_up = thumbs_up_mock - handler.thumbs_down = thumbs_down_mock - handler.sleep = sleep_mock track_uri = TrackUri.from_track(playlist_item_mock).uri handler.process_click(config['pandora']['on_pause_resume_click'], track_uri) - handler.process_click(config['pandora']['on_pause_next_click'], track_uri) - handler.process_click(config['pandora']['on_pause_previous_click'], track_uri) token = PandoraUri.parse(track_uri).token handler.thumbs_up.assert_called_once_with(token) + + +def test_process_click_next(config, handler, playlist_item_mock): + + thumbs_down_mock = mock.PropertyMock() + + handler.thumbs_down = thumbs_down_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + + handler.process_click(config['pandora']['on_pause_next_click'], track_uri) + + token = PandoraUri.parse(track_uri).token handler.thumbs_down.assert_called_once_with(token) + + +def test_process_click_previous(config, handler, playlist_item_mock): + + sleep_mock = mock.PropertyMock() + + handler.sleep = sleep_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + + handler.process_click(config['pandora']['on_pause_previous_click'], track_uri) + + token = PandoraUri.parse(track_uri).token handler.sleep.assert_called_once_with(token) diff --git a/tests/test_playback.py b/tests/test_playback.py index f3d853b..6903007 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -47,50 +47,75 @@ def test_change_track_aborts_if_no_track_uri(provider): def test_pause_starts_double_click_timer(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - assert provider.backend.supports_events - assert provider._double_click_handler.click_time == 0 - provider.pause() - assert provider._double_click_handler.click_time > 0 + assert provider.backend.supports_events + assert provider._double_click_handler.get_click_time() == 0 + provider.pause() + assert provider._double_click_handler.get_click_time() > 0 + + +def test_pause_does_not_start_timer_at_track_start(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=0): + + assert provider.backend.supports_events + assert provider._double_click_handler.get_click_time() == 0 + provider.pause() + assert provider._double_click_handler.get_click_time() == 0 def test_resume_checks_for_double_click(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): + + assert provider.backend.supports_events + is_double_click_mock = mock.PropertyMock() + process_click_mock = mock.PropertyMock() + provider._double_click_handler.is_double_click = is_double_click_mock + provider._double_click_handler.process_click = process_click_mock + provider.resume() + + provider._double_click_handler.is_double_click.assert_called_once_with(100) + + +def test_resume_double_click_call(config, provider): assert provider.backend.supports_events - is_double_click_mock = mock.PropertyMock() + process_click_mock = mock.PropertyMock() - provider._double_click_handler.is_double_click = is_double_click_mock + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time() provider.resume() - provider._double_click_handler.is_double_click.assert_called_once_with() + provider._double_click_handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], + provider.active_track_uri) -def test_resume_double_click_call(config, provider): +def test_resume_resets_click_time(config, provider): assert provider.backend.supports_events process_click_mock = mock.PropertyMock() provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click() + provider._double_click_handler.set_click_time() provider.resume() - provider._double_click_handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], - provider.active_track_uri) + assert provider._double_click_handler.get_click_time() == 0 def test_change_track_checks_for_double_click(provider): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - assert provider.backend.supports_events - is_double_click_mock = mock.PropertyMock() - process_click_mock = mock.PropertyMock() - provider._double_click_handler.is_double_click = is_double_click_mock - provider._double_click_handler.process_click = process_click_mock - provider.change_track(models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri)) + assert provider.backend.supports_events + is_double_click_mock = mock.PropertyMock() + process_click_mock = mock.PropertyMock() + provider._double_click_handler.is_double_click = is_double_click_mock + provider._double_click_handler.process_click = process_click_mock + provider.change_track(models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri)) - provider._double_click_handler.is_double_click.assert_called_once_with() + provider._double_click_handler.is_double_click.assert_called_once_with() def test_change_track_double_click_call(config, provider, playlist_item_mock): @@ -104,13 +129,15 @@ def test_change_track_double_click_call(config, provider, playlist_item_mock): process_click_mock = mock.PropertyMock() provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click() + provider._double_click_handler.set_click_time() provider.active_track_uri = track_0 provider.change_track(models.Track(uri=track_1)) provider._double_click_handler.process_click.assert_called_once_with(config['pandora']['on_pause_next_click'], provider.active_track_uri) + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_1 provider.change_track(models.Track(uri=track_0)) @@ -156,6 +183,41 @@ def test_change_track_handles_request_exceptions(config, caplog): assert 'Error changing track' in caplog.text() +def test_change_track_resets_click_time(config, provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + assert provider.backend.supports_events + + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + process_click_mock = mock.PropertyMock() + + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) + + assert provider._double_click_handler.get_click_time() == 0 + + +def test_change_track_resumes_playback(config, provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + assert provider.backend.supports_events + + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + process_click_mock = mock.PropertyMock() + + provider._double_click_handler.process_click = process_click_mock + provider.backend.rpc_client.resume_playback = mock.PropertyMock() + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) + + provider.backend.rpc_client.resume_playback.assert_called_once_with() + + def test_is_playable_handles_request_exceptions(provider, caplog): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): @@ -176,7 +238,7 @@ def test_auto_set_repeat_off_for_non_pandora_uri(provider): with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="not_a_pandora_uri::::::"): - provider.callback() + provider.prepare_change() assert not provider.backend.rpc_client.set_repeat.called @@ -185,6 +247,6 @@ def test_auto_set_repeat_on_for_pandora_uri(provider): with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): - provider.callback() + provider.prepare_change() provider.backend.rpc_client.set_repeat.assert_called_once_with() From c0a45904cd48bc8c97bdc8665cbc13cf6ceec982 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 11:16:59 +0200 Subject: [PATCH 02/25] Mock the remote procedure call to prevent tests from initiating HTTP connections. --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index da5c4d6..68d735e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,6 +66,8 @@ def config(): def get_backend(config, simulate_request_exceptions=False): obj = backend.PandoraBackend(config=config, audio=Mock()) + obj.rpc_client._do_rpc = Mock() + if simulate_request_exceptions: type(obj.api.transport).__call__ = request_exception_mock else: From dc5bb5ba9eadcdc124b6337810bcc5d7f03a1bfe Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 11:40:29 +0200 Subject: [PATCH 03/25] Mock the remote procedure call to prevent tests from initiating HTTP connections. --- README.rst | 11 +- mopidy_pandora/__init__.py | 2 +- mopidy_pandora/backend.py | 6 +- mopidy_pandora/doubleclick.py | 24 +++-- mopidy_pandora/ext.conf | 2 +- mopidy_pandora/playback.py | 23 ++-- mopidy_pandora/rpc.py | 16 ++- tests/conftest.py | 2 +- tests/test_doubleclick.py | 23 +++- tests/test_playback.py | 190 +++++++++++++++++++++++++--------- 10 files changed, 217 insertions(+), 82 deletions(-) diff --git a/README.rst b/README.rst index cc55bb3..89e7b11 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ Mopidy-Pandora to your Mopidy configuration file:: username = password = sort_order = date - auto_set_repeat = true + auto_setup = true ### EXPERIMENTAL EVENT HANDLING IMPLEMENTATION ### event_support_enabled = false @@ -74,7 +74,7 @@ alphabetical order. and web extensions: - double_click_interval - successive button clicks that occur within this interval (in seconds) will trigger the event. -- on_pause_resume_click - click pause and then play while a song is playing to trigger the event +- on_pause_resume_click - click pause and then play while a song is playing to trigger the event. - on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. - on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. @@ -85,8 +85,9 @@ Usage Mopidy needs `dynamic playlist `_ and `core extensions `_ support to properly support Pandora. In the meantime, -Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode**, -and Mopidy-Pandora will enable this automatically unless you set the **auto_set_repeat** config parameter to 'false'. +Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode** +and **consume**, **shuffle**, and **single** should be turned off. Mopidy-Pandora will set this up automatically unless +you set the **auto_setup** config parameter to 'false'. Each time a track is played, the next dynamic track for that Pandora station will be played. The playlist will consist of a single track unless the experimental ratings support is enabled. With ratings support enabled, the playlist will @@ -110,6 +111,8 @@ v0.1.6 (UNRELEASED) ---------------------------------------- - Fix to resume playback after a track has been rated. +- Changed auto_setup routines to also ensure that 'random' and 'consome' modes are disabled. +- Optimized auto_setup routines: now only called once and only when the first pandora track starts to play. v0.1.5 (UNRELEASED) ---------------------------------------- diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index 2753220..e6fb141 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -37,7 +37,7 @@ def get_config_schema(self): BaseAPIClient.MED_AUDIO_QUALITY, BaseAPIClient.HIGH_AUDIO_QUALITY]) schema['sort_order'] = config.String(optional=True, choices=['date', 'A-Z', 'a-z']) - schema['auto_set_repeat'] = config.Boolean() + schema['auto_setup'] = config.Boolean() schema['event_support_enabled'] = config.Boolean() schema['double_click_interval'] = config.String() schema['on_pause_resume_click'] = config.String(choices=['thumbs_up', 'thumbs_down', 'sleep']) diff --git a/mopidy_pandora/backend.py b/mopidy_pandora/backend.py index 54efcff..d5f0873 100644 --- a/mopidy_pandora/backend.py +++ b/mopidy_pandora/backend.py @@ -33,7 +33,7 @@ def __init__(self, config, audio): self.library = PandoraLibraryProvider(backend=self, sort_order=self._config['sort_order']) - self.auto_set_repeat = self._config['auto_set_repeat'] + self.reset_auto_setup() self.rpc_client = rpc.RPCClient(config['http']['hostname'], config['http']['port']) self.supports_events = False @@ -50,3 +50,7 @@ def on_start(self): self.api.login(self._config["username"], self._config["password"]) except requests.exceptions.RequestException as e: logger.error('Error logging in to Pandora: %s', encoding.locale_decode(e)) + + def reset_auto_setup(self): + self.auto_setup = self._config['auto_setup'] + return self.auto_setup diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index 5fc1a9a..bf39b2e 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -25,23 +25,21 @@ def set_click_time(self, click_time=None): def get_click_time(self): return self._click_time - def is_double_click(self, time_position=None): + def is_double_click(self): - if self._click_time == 0: - return False + double_clicked = self._click_time > 0 and time.time() - self._click_time < float(self.double_click_interval) - if time_position is None: - time_position = 1 + if double_clicked is False: + self._click_time = 0 - return time.time() - self._click_time < float(self.double_click_interval) and time_position > 0 + return double_clicked def on_change_track(self, active_track_uri, new_track_uri): from mopidy_pandora.uri import PandoraUri if not self.is_double_click(): - return + return False - # TODO: Won't work if 'shuffle' or 'consume' modes are enabled if active_track_uri is not None: new_track_index = int(PandoraUri.parse(new_track_uri).index) @@ -53,12 +51,16 @@ def on_change_track(self, active_track_uri, new_track_uri): elif new_track_index < active_track_index or new_track_index == active_track_index: self.process_click(self.on_pause_previous_click, active_track_uri) + return True + def on_resume_click(self, track_uri, time_position): - if not self.is_double_click(time_position): - return + if not self.is_double_click() or time_position == 0: + return False self.process_click(self.on_pause_resume_click, track_uri) + return True + def process_click(self, method, track_uri): uri = PandoraUri.parse(track_uri) @@ -66,6 +68,8 @@ def process_click(self, method, track_uri): func = getattr(self, method) func(uri.token) + self.set_click_time(0) + def thumbs_up(self, track_token): self.client.add_feedback(track_token, True) diff --git a/mopidy_pandora/ext.conf b/mopidy_pandora/ext.conf index 832a238..c125607 100644 --- a/mopidy_pandora/ext.conf +++ b/mopidy_pandora/ext.conf @@ -10,7 +10,7 @@ username = password = preferred_audio_quality = highQuality sort_order = date -auto_set_repeat = true +auto_setup = true ### EXPERIMENTAL RATINGS IMPLEMENTATION ### event_support_enabled = false diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index db10ff3..5fc7f51 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -26,18 +26,25 @@ def __init__(self, audio, backend): # See: https://discuss.mopidy.com/t/has-the-gapless-playback-implementation-been-completed-yet/784/2 # self.audio.set_uri(self.translate_uri(self.get_next_track())).get() - def _auto_set_repeat(self): + def _auto_setup(self): uri = self.backend.rpc_client.get_current_track_uri() # Make sure that tracks are being played in 'repeat mode'. if uri is not None and uri.startswith("pandora:"): - self.backend.rpc_client.set_repeat() + if self.backend.auto_setup: + self.backend.rpc_client.set_repeat() + self.backend.rpc_client.set_consume(False) + self.backend.rpc_client.set_random(False) + self.backend.rpc_client.set_single(False) + self.backend.auto_setup = False + + else: + self.backend.reset_auto_setup() def prepare_change(self): - if self.backend.auto_set_repeat: - Thread(target=self._auto_set_repeat).start() + Thread(target=self._auto_setup).start() super(PandoraPlaybackProvider, self).prepare_change() @@ -95,11 +102,10 @@ def __init__(self, audio, backend): def change_track(self, track): - self._double_click_handler.on_change_track(self.active_track_uri, track.uri) + event_processed = self._double_click_handler.on_change_track(self.active_track_uri, track.uri) return_value = super(EventSupportPlaybackProvider, self).change_track(track) - if self._double_click_handler.get_click_time() > 0: - self._double_click_handler.set_click_time(0) + if event_processed: Thread(target=self.backend.rpc_client.resume_playback).start() return return_value @@ -114,7 +120,4 @@ def pause(self): def resume(self): self._double_click_handler.on_resume_click(self.active_track_uri, self.get_time_position()) - if self._double_click_handler.get_click_time() > 0: - self._double_click_handler.set_click_time(0) - return super(EventSupportPlaybackProvider, self).resume() diff --git a/mopidy_pandora/rpc.py b/mopidy_pandora/rpc.py index 19ebcb7..2a1b728 100644 --- a/mopidy_pandora/rpc.py +++ b/mopidy_pandora/rpc.py @@ -18,9 +18,21 @@ def _do_rpc(self, method, params=None): return requests.request('POST', self.url, data=json.dumps(data), headers={'Content-Type': 'application/json'}) - def set_repeat(self): + def set_repeat(self, value=True): - self._do_rpc('core.tracklist.set_repeat', {'value': True}) + self._do_rpc('core.tracklist.set_repeat', {'value': value}) + + def set_consume(self, value=True): + + self._do_rpc('core.tracklist.set_consume', {'value': value}) + + def set_single(self, value=True): + + self._do_rpc('core.tracklist.set_single', {'value': value}) + + def set_random(self, value=True): + + self._do_rpc('core.tracklist.set_random', {'value': value}) def get_current_track_uri(self): diff --git a/tests/conftest.py b/tests/conftest.py index 68d735e..0df7268 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ def config(): 'password': 'doe', 'preferred_audio_quality': MOCK_DEFAULT_AUDIO_QUALITY, 'sort_order': 'date', - 'auto_set_repeat': True, + 'auto_setup': True, 'event_support_enabled': True, 'double_click_interval': '0.1', diff --git a/tests/test_doubleclick.py b/tests/test_doubleclick.py index 2323b9e..a508221 100644 --- a/tests/test_doubleclick.py +++ b/tests/test_doubleclick.py @@ -39,11 +39,19 @@ def test_is_double_click(handler): assert handler.is_double_click() - assert handler.is_double_click(0) is False + time.sleep(float(handler.double_click_interval) + 0.1) + assert handler.is_double_click() is False + + +def test_is_double_click_resets_click_time(handler): + + assert handler.is_double_click() time.sleep(float(handler.double_click_interval) + 0.1) assert handler.is_double_click() is False + assert handler.get_click_time() == 0 + def test_on_change_track_forward(config, handler, playlist_item_mock): @@ -100,6 +108,19 @@ def test_on_resume_click(config, handler, playlist_item_mock): handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], track_uri) +def test_process_click_resets_click_time(config, handler, playlist_item_mock): + + thumbs_up_mock = mock.PropertyMock() + + handler.thumbs_up = thumbs_up_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + + handler.process_click(config['pandora']['on_pause_resume_click'], track_uri) + + assert handler.get_click_time() == 0 + + def test_process_click_resume(config, handler, playlist_item_mock): thumbs_up_mock = mock.PropertyMock() diff --git a/tests/test_playback.py b/tests/test_playback.py index 6903007..48314d2 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import threading + import conftest import mock @@ -48,7 +50,6 @@ def test_change_track_aborts_if_no_track_uri(provider): def test_pause_starts_double_click_timer(provider): with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - assert provider.backend.supports_events assert provider._double_click_handler.get_click_time() == 0 provider.pause() @@ -57,7 +58,6 @@ def test_pause_starts_double_click_timer(provider): def test_pause_does_not_start_timer_at_track_start(provider): with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=0): - assert provider.backend.supports_events assert provider._double_click_handler.get_click_time() == 0 provider.pause() @@ -66,7 +66,6 @@ def test_pause_does_not_start_timer_at_track_start(provider): def test_resume_checks_for_double_click(provider): with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - assert provider.backend.supports_events is_double_click_mock = mock.PropertyMock() process_click_mock = mock.PropertyMock() @@ -74,11 +73,10 @@ def test_resume_checks_for_double_click(provider): provider._double_click_handler.process_click = process_click_mock provider.resume() - provider._double_click_handler.is_double_click.assert_called_once_with(100) + provider._double_click_handler.is_double_click.assert_called_once_with() def test_resume_double_click_call(config, provider): - assert provider.backend.supports_events process_click_mock = mock.PropertyMock() @@ -91,28 +89,15 @@ def test_resume_double_click_call(config, provider): provider.active_track_uri) -def test_resume_resets_click_time(config, provider): - - assert provider.backend.supports_events - - process_click_mock = mock.PropertyMock() - - provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click_time() - provider.resume() - - assert provider._double_click_handler.get_click_time() == 0 - - def test_change_track_checks_for_double_click(provider): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - assert provider.backend.supports_events is_double_click_mock = mock.PropertyMock() process_click_mock = mock.PropertyMock() provider._double_click_handler.is_double_click = is_double_click_mock provider._double_click_handler.process_click = process_click_mock + provider.backend.rpc_client.resume_playback = mock.PropertyMock() provider.change_track(models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri)) provider._double_click_handler.is_double_click.assert_called_once_with() @@ -129,6 +114,7 @@ def test_change_track_double_click_call(config, provider, playlist_item_mock): process_click_mock = mock.PropertyMock() provider._double_click_handler.process_click = process_click_mock + provider.backend.rpc_client.resume_playback = mock.PropertyMock() provider._double_click_handler.set_click_time() provider.active_track_uri = track_0 provider.change_track(models.Track(uri=track_1)) @@ -149,7 +135,6 @@ def test_change_track(audio_mock, provider): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', return_value=True): - track = models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri) assert provider.change_track(track) is True @@ -164,7 +149,6 @@ def test_change_track_enforces_skip_limit(provider): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', return_value=False): - track = models.Track(uri="pandora:track:test::::") assert provider.change_track(track) is False @@ -174,7 +158,6 @@ def test_change_track_enforces_skip_limit(provider): def test_change_track_handles_request_exceptions(config, caplog): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.request_exception_mock): - track = models.Track(uri="pandora:track:test::::") playback = conftest.get_backend(config).playback @@ -183,46 +166,67 @@ def test_change_track_handles_request_exceptions(config, caplog): assert 'Error changing track' in caplog.text() -def test_change_track_resets_click_time(config, provider, playlist_item_mock): +def test_change_track_resumes_playback(provider, playlist_item_mock): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): - assert provider.backend.supports_events + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events - track_0 = TrackUri.from_track(playlist_item_mock, 0).uri - track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + event = threading.Event() - process_click_mock = mock.PropertyMock() + def set_event(): + event.set() - provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click_time() - provider.active_track_uri = track_0 - provider.change_track(models.Track(uri=track_1)) + mock_rpc.side_effect = set_event - assert provider._double_click_handler.get_click_time() == 0 + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + process_click_mock = mock.PropertyMock() -def test_change_track_resumes_playback(config, provider, playlist_item_mock): + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + + provider.change_track(models.Track(uri=track_1)) + + if event.wait(timeout=1.0): + mock_rpc.assert_called_once_with() + else: + assert False + + +def test_change_track_does_not_resume_playback_if_not_doubleclick(provider, playlist_item_mock): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): - assert provider.backend.supports_events + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events - track_0 = TrackUri.from_track(playlist_item_mock, 0).uri - track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + event = threading.Event() - process_click_mock = mock.PropertyMock() + def set_event(): + event.set() - provider._double_click_handler.process_click = process_click_mock - provider.backend.rpc_client.resume_playback = mock.PropertyMock() - provider._double_click_handler.set_click_time() - provider.active_track_uri = track_0 - provider.change_track(models.Track(uri=track_1)) + mock_rpc.side_effect = set_event - provider.backend.rpc_client.resume_playback.assert_called_once_with() + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + process_click_mock = mock.PropertyMock() + + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time(0) + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) + + if event.wait(timeout=1.0): + assert False + else: + assert not mock_rpc.called def test_is_playable_handles_request_exceptions(provider, caplog): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', conftest.request_exception_mock): - track = models.Track(uri="pandora:track:test::::") assert provider.change_track(track) is False @@ -230,23 +234,107 @@ def test_is_playable_handles_request_exceptions(provider, caplog): def test_translate_uri_returns_audio_url(provider): - assert provider.translate_uri("pandora:track:test:::::audio_url") == "audio_url" -def test_auto_set_repeat_off_for_non_pandora_uri(provider): - with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): +def test_auto_setup_off_for_non_pandora_uri(provider): + with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, + set_consume=mock.DEFAULT) as values: with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="not_a_pandora_uri::::::"): + event = threading.Event() + + def set_event(): + event.set() + + values['set_repeat'].side_effect = set_event + provider.prepare_change() - assert not provider.backend.rpc_client.set_repeat.called + if event.wait(timeout=1.0): + assert False + else: + assert not values['set_repeat'].called + assert not values['set_random'].called + assert not values['set_consume'].called + +def test_auto_setup_on_for_pandora_uri(provider): + with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, + set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: -def test_auto_set_repeat_on_for_pandora_uri(provider): - with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): + event = threading.Event() + + def set_event(): + event.set() + + values['set_repeat'].side_effect = set_event + + provider.prepare_change() + + if event.wait(timeout=1.0): + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) + else: + assert False + + +def test_auto_setup_only_called_once(provider): + with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, + set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: + with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): + + event = threading.Event() + + def set_event(): + event.set() + + values['set_repeat'].side_effect = set_event + + provider.prepare_change() + provider.prepare_change() + + if event.wait(timeout=1.0): + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) + else: + assert False + + +def test_auto_setup_resets_for_non_pandora_tracks(provider): + with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, + set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: + with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::") as mock_get_uri: + + event = threading.Event() + + def set_event(): + event.set() + + values['set_repeat'].side_effect = set_event + + provider.prepare_change() + + if event.wait(timeout=1.0): + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) + assert not provider.backend.auto_setup + else: + assert False + + mock_get_uri.return_value = "not_a_pandora_uri::::::" + provider.prepare_change() - provider.backend.rpc_client.set_repeat.assert_called_once_with() + if event.wait(timeout=1.0): + assert provider.backend.auto_setup + else: + assert False From 9e769d6c03113b7818c30ccfcf4db5cc1ff7104f Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 15:02:55 +0200 Subject: [PATCH 04/25] Fix typos. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 89e7b11..708fb32 100644 --- a/README.rst +++ b/README.rst @@ -111,8 +111,8 @@ v0.1.6 (UNRELEASED) ---------------------------------------- - Fix to resume playback after a track has been rated. -- Changed auto_setup routines to also ensure that 'random' and 'consome' modes are disabled. -- Optimized auto_setup routines: now only called once and only when the first pandora track starts to play. +- Changed auto_setup routines to also ensure that 'consume', 'shuffle', and 'single' modes are disabled. +- Optimized auto_setup routines: now only called once, and only when the first pandora track starts to play. v0.1.5 (UNRELEASED) ---------------------------------------- From aa1f2e078fb6601282332dee4965cbdfce405efd Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 16:19:26 +0200 Subject: [PATCH 05/25] Fix typos. --- README.rst | 7 ++++++- mopidy_pandora/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 708fb32..8d3d1a2 100644 --- a/README.rst +++ b/README.rst @@ -107,13 +107,18 @@ Project resources Changelog ========= -v0.1.6 (UNRELEASED) +v0.1.7 (UNRELEASED) ---------------------------------------- - Fix to resume playback after a track has been rated. - Changed auto_setup routines to also ensure that 'consume', 'shuffle', and 'single' modes are disabled. - Optimized auto_setup routines: now only called once, and only when the first pandora track starts to play. +v0.1.6 (Oct 26, 2015) +---------------------------------------- + +- Release to pypi + v0.1.5 (UNRELEASED) ---------------------------------------- diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index e6fb141..c7a95b5 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -8,7 +8,7 @@ from pandora import BaseAPIClient -__version__ = '0.1.6' +__version__ = '0.1.7' logger = logging.getLogger(__name__) From b2aeac59a3853d5f9d3bb6f93937071ad00c0134 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 16:23:46 +0200 Subject: [PATCH 06/25] Roll back version number. --- mopidy_pandora/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index c7a95b5..e6fb141 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -8,7 +8,7 @@ from pandora import BaseAPIClient -__version__ = '0.1.7' +__version__ = '0.1.6' logger = logging.getLogger(__name__) From 4db00b60c7e07e6cafc423fdccb288a2aba617f1 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 17:05:22 +0200 Subject: [PATCH 07/25] Increase test timeouts. --- tests/test_playback.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/test_playback.py b/tests/test_playback.py index 48314d2..3db7340 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -239,24 +239,25 @@ def test_translate_uri_returns_audio_url(provider): def test_auto_setup_off_for_non_pandora_uri(provider): with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, - set_consume=mock.DEFAULT) as values: + set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="not_a_pandora_uri::::::"): event = threading.Event() - def set_event(): + def set_event(*args, **kwargs): event.set() - values['set_repeat'].side_effect = set_event + values['set_single'].side_effect = set_event provider.prepare_change() - if event.wait(timeout=1.0): + if event.wait(timeout=5.0): assert False else: assert not values['set_repeat'].called assert not values['set_random'].called assert not values['set_consume'].called + assert not values['set_single'].called def test_auto_setup_on_for_pandora_uri(provider): @@ -267,14 +268,14 @@ def test_auto_setup_on_for_pandora_uri(provider): event = threading.Event() - def set_event(): + def set_event(*args, **kwargs): event.set() - values['set_repeat'].side_effect = set_event + values['set_single'].side_effect = set_event provider.prepare_change() - if event.wait(timeout=1.0): + if event.wait(timeout=5.0): values['set_repeat'].assert_called_once_with() values['set_random'].assert_called_once_with(False) values['set_consume'].assert_called_once_with(False) @@ -290,15 +291,15 @@ def test_auto_setup_only_called_once(provider): event = threading.Event() - def set_event(): + def set_event(*args, **kwargs): event.set() - values['set_repeat'].side_effect = set_event + values['set_single'].side_effect = set_event provider.prepare_change() provider.prepare_change() - if event.wait(timeout=1.0): + if event.wait(timeout=5.0): values['set_repeat'].assert_called_once_with() values['set_random'].assert_called_once_with(False) values['set_consume'].assert_called_once_with(False) @@ -314,14 +315,14 @@ def test_auto_setup_resets_for_non_pandora_tracks(provider): event = threading.Event() - def set_event(): + def set_event(*args, **kwargs): event.set() - values['set_repeat'].side_effect = set_event + values['set_single'].side_effect = set_event provider.prepare_change() - if event.wait(timeout=1.0): + if event.wait(timeout=5.0): values['set_repeat'].assert_called_once_with() values['set_random'].assert_called_once_with(False) values['set_consume'].assert_called_once_with(False) From 69283c5368001c3d59be186095ea5e9b24e59041 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 17:27:52 +0200 Subject: [PATCH 08/25] Ensure test failure on actual RPC calls. --- tests/conftest.py | 11 ++++++++++- tests/test_playback.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0df7268..7a3b6a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,7 +66,7 @@ def config(): def get_backend(config, simulate_request_exceptions=False): obj = backend.PandoraBackend(config=config, audio=Mock()) - obj.rpc_client._do_rpc = Mock() + obj.rpc_client._do_rpc = rpc_call_not_implemented_mock if simulate_request_exceptions: type(obj.api.transport).__call__ = request_exception_mock @@ -193,3 +193,12 @@ def transport_call_not_implemented_mock(self, method, **data): class TransportCallTestNotImplemented(Exception): pass + + +@pytest.fixture +def rpc_call_not_implemented_mock(self, method, params=None): + raise RPCCallTestNotImplemented(method + "(" + params + ")") + + +class RPCCallTestNotImplemented(Exception): + pass diff --git a/tests/test_playback.py b/tests/test_playback.py index 3db7340..5a28f08 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -336,6 +336,6 @@ def set_event(*args, **kwargs): provider.prepare_change() if event.wait(timeout=1.0): - assert provider.backend.auto_setup + assert provider.backend.auto_setup is True else: assert False From 6741f1868a82eb0605597979b5c502d111671535 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 17:37:39 +0200 Subject: [PATCH 09/25] Ensure test failure on actual RPC calls. --- tests/conftest.py | 2 +- tests/test_playback.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a3b6a2..9748cc8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,7 +196,7 @@ class TransportCallTestNotImplemented(Exception): @pytest.fixture -def rpc_call_not_implemented_mock(self, method, params=None): +def rpc_call_not_implemented_mock(method, params=None): raise RPCCallTestNotImplemented(method + "(" + params + ")") diff --git a/tests/test_playback.py b/tests/test_playback.py index 5a28f08..0b83d98 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -311,7 +311,7 @@ def set_event(*args, **kwargs): def test_auto_setup_resets_for_non_pandora_tracks(provider): with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::") as mock_get_uri: + with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): event = threading.Event() @@ -331,7 +331,7 @@ def set_event(*args, **kwargs): else: assert False - mock_get_uri.return_value = "not_a_pandora_uri::::::" + provider.backend.rpc_client.get_current_track_uri = mock.Mock(return_value="not_a_pandora_uri::::::") provider.prepare_change() From 7f922bd6b9dd3aed48892d06f12f152500e6c5d6 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 18:02:25 +0200 Subject: [PATCH 10/25] Add release dates to README file. --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 8d3d1a2..a9e15b1 100644 --- a/README.rst +++ b/README.rst @@ -119,7 +119,7 @@ v0.1.6 (Oct 26, 2015) - Release to pypi -v0.1.5 (UNRELEASED) +v0.1.5 (Aug 20, 2015) ---------------------------------------- - Add option to automatically set tracks to play in repeat mode when Mopidy-Pandora starts. @@ -129,14 +129,14 @@ v0.1.5 (UNRELEASED) - Fix to retrieve stations by ID instead of token. - Add unit tests to increase test coverage. -v0.1.4 (UNRELEASED) +v0.1.4 (Aug 17, 2015) ---------------------------------------- - Limit number of consecutive track skips to prevent Mopidy's skip-to-next-on-error behaviour from locking the user's Pandora account. - Better handling of exceptions that occur in the backend to prevent Mopidy actor crashes. - Add support for unicode characters in station and track names. -v0.1.3 (UNRELEASED) +v0.1.3 (Jul 11, 2015) ---------------------------------------- - Update to work with release of Mopidy version 1.0 @@ -145,19 +145,19 @@ v0.1.3 (UNRELEASED) - Get rid of 'Stations' root directory. Browsing now displays all of the available stations immediately. - Fill artist name to improve how tracks are displayed in various Mopidy front-end extensions. -v0.1.2 (UNRELEASED) +v0.1.2 (Jun 20, 2015) ---------------------------------------- - Enhancement to handle 'Invalid Auth Token' exceptions when the Pandora session expires after long periods of inactivity. Allows Mopidy-Pandora to run indefinitely on dedicated music servers like the Pi MusicBox. - Add configuration option to sort stations alphabetically, instead of by date. -v0.1.1 (UNRELEASED) +v0.1.1 (Mar 22, 2015) ---------------------------------------- - Added ability to make preferred audio quality user-configurable. -v0.1.0 (UNRELEASED) +v0.1.0 (Dec 28, 2014) ---------------------------------------- - Initial release. From 3d975015a3fd835a0cba9f138ade246d7da004ef Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 26 Oct 2015 18:06:42 +0200 Subject: [PATCH 11/25] Only set up player when the tracklist changes. --- README.rst | 1 + mopidy_pandora/__init__.py | 2 + mopidy_pandora/backend.py | 13 ++-- mopidy_pandora/client.py | 29 +++++++++ mopidy_pandora/doubleclick.py | 30 +++++---- mopidy_pandora/playback.py | 20 +++--- tests/conftest.py | 29 +++++++-- tests/test_backend.py | 11 ++++ tests/test_client.py | 36 +++++++++++ tests/test_playback.py | 114 ++++++++++------------------------ 10 files changed, 167 insertions(+), 118 deletions(-) diff --git a/README.rst b/README.rst index a9e15b1..b3e76bc 100644 --- a/README.rst +++ b/README.rst @@ -110,6 +110,7 @@ Changelog v0.1.7 (UNRELEASED) ---------------------------------------- +- Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file. - Fix to resume playback after a track has been rated. - Changed auto_setup routines to also ensure that 'consume', 'shuffle', and 'single' modes are disabled. - Optimized auto_setup routines: now only called once, and only when the first pandora track starts to play. diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index e6fb141..d3ade1e 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -4,6 +4,7 @@ import os from mopidy import config, ext +from mopidy.config import Deprecated from pandora import BaseAPIClient @@ -38,6 +39,7 @@ def get_config_schema(self): BaseAPIClient.HIGH_AUDIO_QUALITY]) schema['sort_order'] = config.String(optional=True, choices=['date', 'A-Z', 'a-z']) schema['auto_setup'] = config.Boolean() + schema['auto_set_repeat'] = Deprecated() schema['event_support_enabled'] = config.Boolean() schema['double_click_interval'] = config.String() schema['on_pause_resume_click'] = config.String(choices=['thumbs_up', 'thumbs_down', 'sleep']) diff --git a/mopidy_pandora/backend.py b/mopidy_pandora/backend.py index d5f0873..1fa60bb 100644 --- a/mopidy_pandora/backend.py +++ b/mopidy_pandora/backend.py @@ -1,4 +1,4 @@ -from mopidy import backend +from mopidy import backend, core from mopidy.internal import encoding from pandora import BaseAPIClient, clientbuilder @@ -15,7 +15,7 @@ from mopidy_pandora.uri import logger -class PandoraBackend(pykka.ThreadingActor, backend.Backend): +class PandoraBackend(pykka.ThreadingActor, backend.Backend, core.CoreListener): def __init__(self, config, audio): super(PandoraBackend, self).__init__() @@ -33,7 +33,9 @@ def __init__(self, config, audio): self.library = PandoraLibraryProvider(backend=self, sort_order=self._config['sort_order']) - self.reset_auto_setup() + self.auto_setup = self._config['auto_setup'] + self.setup_required = self.auto_setup + self.rpc_client = rpc.RPCClient(config['http']['hostname'], config['http']['port']) self.supports_events = False @@ -51,6 +53,5 @@ def on_start(self): except requests.exceptions.RequestException as e: logger.error('Error logging in to Pandora: %s', encoding.locale_decode(e)) - def reset_auto_setup(self): - self.auto_setup = self._config['auto_setup'] - return self.auto_setup + def tracklist_changed(self): + self.setup_required = self.auto_setup diff --git a/mopidy_pandora/client.py b/mopidy_pandora/client.py index b396ba0..8afef57 100644 --- a/mopidy_pandora/client.py +++ b/mopidy_pandora/client.py @@ -39,3 +39,32 @@ def get_station(self, station_id): except TypeError: # Could not find station_id in cached list, try retrieving from Pandora server. return super(MopidyPandoraAPIClient, self).get_station(station_id) + + +class PandoraResult(object): + + """Object for storing results of API calls for easy reference""" + + def __init__(self, result): + + self._raw_result = result + self.status_ok = False + self.message = "" + self.code = 0000 + + if str(result['stat']).upper() == 'OK': + self.status_ok = True + else: + self.status_ok = False + + try: + self.message = result['message'] + except KeyError as e: + if not self.status_ok: + raise e + + try: + self.code = result['code'] + except KeyError as e: + if not self.status_ok: + raise e diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index bf39b2e..d8307fe 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -2,6 +2,7 @@ import time +from mopidy_pandora.client import PandoraResult from mopidy_pandora.library import PandoraUri logger = logging.getLogger(__name__) @@ -46,41 +47,44 @@ def on_change_track(self, active_track_uri, new_track_uri): active_track_index = int(PandoraUri.parse(active_track_uri).index) if new_track_index > active_track_index or new_track_index == 0 and active_track_index == 2: - self.process_click(self.on_pause_next_click, active_track_uri) + return self.process_click(self.on_pause_next_click, active_track_uri) elif new_track_index < active_track_index or new_track_index == active_track_index: - self.process_click(self.on_pause_previous_click, active_track_uri) + return self.process_click(self.on_pause_previous_click, active_track_uri) - return True + return False def on_resume_click(self, track_uri, time_position): if not self.is_double_click() or time_position == 0: return False - self.process_click(self.on_pause_resume_click, track_uri) - - return True + return self.process_click(self.on_pause_resume_click, track_uri) def process_click(self, method, track_uri): + self.set_click_time(0) + uri = PandoraUri.parse(track_uri) logger.info("Triggering event '%s' for song: %s", method, uri.name) func = getattr(self, method) - func(uri.token) + result = PandoraResult(func(uri.token)) - self.set_click_time(0) + if not result.status_ok: + logger.error('Error calling event: %s (code %s)', result.message, result.code) + + return result.status_ok def thumbs_up(self, track_token): - self.client.add_feedback(track_token, True) + return self.client.add_feedback(track_token, True) def thumbs_down(self, track_token): - self.client.add_feedback(track_token, False) + return self.client.add_feedback(track_token, False) def sleep(self, track_token): - self.client.sleep_song(track_token) + return self.client.sleep_song(track_token) def add_artist_bookmark(self, track_token): - self.client.add_artist_bookmark(track_token) + return self.client.add_artist_bookmark(track_token) def add_song_bookmark(self, track_token): - self.client.add_song_bookmark(track_token) + return self.client.add_song_bookmark(track_token) diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index 5fc7f51..46a437a 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -28,23 +28,17 @@ def __init__(self, audio, backend): def _auto_setup(self): - uri = self.backend.rpc_client.get_current_track_uri() + self.backend.rpc_client.set_repeat() + self.backend.rpc_client.set_consume(False) + self.backend.rpc_client.set_random(False) + self.backend.rpc_client.set_single(False) - # Make sure that tracks are being played in 'repeat mode'. - if uri is not None and uri.startswith("pandora:"): - if self.backend.auto_setup: - self.backend.rpc_client.set_repeat() - self.backend.rpc_client.set_consume(False) - self.backend.rpc_client.set_random(False) - self.backend.rpc_client.set_single(False) - self.backend.auto_setup = False - - else: - self.backend.reset_auto_setup() + self.backend.setup_required = False def prepare_change(self): - Thread(target=self._auto_setup).start() + if self.backend.auto_setup and self.backend.setup_required: + Thread(target=self._auto_setup).start() super(PandoraPlaybackProvider, self).prepare_change() diff --git a/tests/conftest.py b/tests/conftest.py index 9748cc8..719e067 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,6 +79,25 @@ def get_backend(config, simulate_request_exceptions=False): return obj +@pytest.fixture(scope="session") +def success_result_mock(): + mock_result = {"stat": "ok", + "result": {} + } + + return mock_result + + +@pytest.fixture(scope="session") +def fail_result_mock(): + mock_result = {"stat": "fail", + "message": "An unexpected error occurred", + "code": 9999 + } + + return mock_result + + @pytest.fixture(scope="session") def station_result_mock(): mock_result = {"stat": "ok", @@ -90,12 +109,12 @@ def station_result_mock(): "stationName": MOCK_STATION_NAME}, } - return mock_result["result"] + return mock_result @pytest.fixture(scope="session") def station_mock(simulate_request_exceptions=False): - return Station.from_json(get_backend(config(), simulate_request_exceptions).api, station_result_mock()) + return Station.from_json(get_backend(config(), simulate_request_exceptions).api, station_result_mock()["result"]) @pytest.fixture(scope="session") @@ -138,12 +157,12 @@ def playlist_result_mock(): "stationId": MOCK_STATION_ID, "songRating": 0, }]}} - return mock_result["result"] + return mock_result @pytest.fixture(scope="session") def playlist_mock(simulate_request_exceptions=False): - return Playlist.from_json(get_backend(config(), simulate_request_exceptions).api, playlist_result_mock()) + return Playlist.from_json(get_backend(config(), simulate_request_exceptions).api, playlist_result_mock()["result"]) @pytest.fixture(scope="session") @@ -159,7 +178,7 @@ def get_station_playlist_mock(self): @pytest.fixture(scope="session") def playlist_item_mock(): return PlaylistItem.from_json(get_backend( - config()).api, playlist_result_mock()["items"][0]) + config()).api, playlist_result_mock()["result"]["items"][0]) @pytest.fixture(scope="session") diff --git a/tests/test_backend.py b/tests/test_backend.py index 4479416..526a170 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -74,3 +74,14 @@ def test_on_start_handles_request_exception(config, caplog): # Check that request exceptions are caught and logged assert 'Error logging in to Pandora' in caplog.text() + + +def test_auto_setup_resets_when_tracklist_changes(config): + + backend = get_backend(config, True) + + backend.setup_required = False + + backend.tracklist_changed() + + assert backend.setup_required diff --git a/tests/test_client.py b/tests/test_client.py index 9576c16..2fd954c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,6 +11,8 @@ import pytest +from mopidy_pandora.client import PandoraResult + from tests.conftest import get_backend from tests.conftest import get_station_list_mock @@ -91,3 +93,37 @@ def test_get_invalid_station(config): backend = get_backend(config) backend.api.get_station("9999999999999999999") + + +def test_pandora_result_success(): + + result = PandoraResult(conftest.success_result_mock()) + assert result.status_ok is True + + +def test_pandora_result_fail(): + + result = PandoraResult(conftest.fail_result_mock()) + assert result.status_ok is False + assert result.message == "An unexpected error occurred" + assert result.code == 9999 + + +def test_pandora_result_no_message_raises_exception(): + with pytest.raises(KeyError): + + inconsitent_result_mock_no_message = {"stat": "fail", + "code": 9999 + } + + PandoraResult(inconsitent_result_mock_no_message) + + +def test_pandora_result_no_code_raises_exception(): + with pytest.raises(KeyError): + + inconsitent_result_mock_no_code = {"stat": "fail", + "message": "An unexpected error occurred", + } + + PandoraResult(inconsitent_result_mock_no_code) diff --git a/tests/test_playback.py b/tests/test_playback.py index 0b83d98..015c778 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import threading +import time import conftest @@ -141,7 +142,7 @@ def test_change_track(audio_mock, provider): assert audio_mock.prepare_change.call_count == 0 assert audio_mock.start_playback.call_count == 0 audio_mock.set_uri.assert_called_once_with(PlaylistItem.get_audio_url( - conftest.playlist_result_mock()["items"][0], + conftest.playlist_result_mock()["result"]["items"][0], conftest.MOCK_DEFAULT_AUDIO_QUALITY)) @@ -195,7 +196,7 @@ def set_event(): assert False -def test_change_track_does_not_resume_playback_if_not_doubleclick(provider, playlist_item_mock): +def test_change_track_does_not_resume_playback_if_not_doubleclick(config, provider, playlist_item_mock): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: assert provider.backend.supports_events @@ -212,8 +213,10 @@ def set_event(): process_click_mock = mock.PropertyMock() + click_interval = float(config['pandora']['double_click_interval']) + 1.0 + provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click_time(0) + provider._double_click_handler.set_click_time(time.time() - click_interval) provider.active_track_uri = track_0 provider.change_track(models.Track(uri=track_1)) @@ -223,65 +226,47 @@ def set_event(): assert not mock_rpc.called -def test_is_playable_handles_request_exceptions(provider, caplog): - with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): - with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): - with mock.patch.object(PlaylistItem, 'get_is_playable', conftest.request_exception_mock): - track = models.Track(uri="pandora:track:test::::") - - assert provider.change_track(track) is False - assert 'Error checking if track is playable' in caplog.text() - - -def test_translate_uri_returns_audio_url(provider): - assert provider.translate_uri("pandora:track:test:::::audio_url") == "audio_url" - - -def test_auto_setup_off_for_non_pandora_uri(provider): - with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, - set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="not_a_pandora_uri::::::"): +def test_change_track_does_not_resume_playback_if_event_failed(provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events event = threading.Event() - def set_event(*args, **kwargs): + def set_event(): event.set() - values['set_single'].side_effect = set_event - - provider.prepare_change() + mock_rpc.side_effect = set_event - if event.wait(timeout=5.0): - assert False - else: - assert not values['set_repeat'].called - assert not values['set_random'].called - assert not values['set_consume'].called - assert not values['set_single'].called + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + process_click_mock = mock.PropertyMock() + process_click_mock.return_value = False -def test_auto_setup_on_for_pandora_uri(provider): - with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, - set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): + if event.wait(timeout=1.0): + assert False + else: + assert not mock_rpc.called - event = threading.Event() - def set_event(*args, **kwargs): - event.set() +def test_is_playable_handles_request_exceptions(provider, caplog): + with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): + with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): + with mock.patch.object(PlaylistItem, 'get_is_playable', conftest.request_exception_mock): + track = models.Track(uri="pandora:track:test::::") - values['set_single'].side_effect = set_event + assert provider.change_track(track) is False + assert 'Error checking if track is playable' in caplog.text() - provider.prepare_change() - if event.wait(timeout=5.0): - values['set_repeat'].assert_called_once_with() - values['set_random'].assert_called_once_with(False) - values['set_consume'].assert_called_once_with(False) - values['set_single'].assert_called_once_with(False) - else: - assert False +def test_translate_uri_returns_audio_url(provider): + assert provider.translate_uri("pandora:track:test:::::audio_url") == "audio_url" def test_auto_setup_only_called_once(provider): @@ -306,36 +291,3 @@ def set_event(*args, **kwargs): values['set_single'].assert_called_once_with(False) else: assert False - - -def test_auto_setup_resets_for_non_pandora_tracks(provider): - with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, - set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): - - event = threading.Event() - - def set_event(*args, **kwargs): - event.set() - - values['set_single'].side_effect = set_event - - provider.prepare_change() - - if event.wait(timeout=5.0): - values['set_repeat'].assert_called_once_with() - values['set_random'].assert_called_once_with(False) - values['set_consume'].assert_called_once_with(False) - values['set_single'].assert_called_once_with(False) - assert not provider.backend.auto_setup - else: - assert False - - provider.backend.rpc_client.get_current_track_uri = mock.Mock(return_value="not_a_pandora_uri::::::") - - provider.prepare_change() - - if event.wait(timeout=1.0): - assert provider.backend.auto_setup is True - else: - assert False From c05eb13b2a4bebc319a253e6ce11bb9bb3977329 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 28 Oct 2015 07:16:33 +0200 Subject: [PATCH 12/25] Add check to ensure that ratings were applied successfully. Handles PandoraException raised by pydora. --- mopidy_pandora/client.py | 29 ---------------------------- mopidy_pandora/doubleclick.py | 16 +++++++++++----- tests/test_client.py | 36 ----------------------------------- tests/test_playback.py | 8 +++++--- 4 files changed, 16 insertions(+), 73 deletions(-) diff --git a/mopidy_pandora/client.py b/mopidy_pandora/client.py index 8afef57..b396ba0 100644 --- a/mopidy_pandora/client.py +++ b/mopidy_pandora/client.py @@ -39,32 +39,3 @@ def get_station(self, station_id): except TypeError: # Could not find station_id in cached list, try retrieving from Pandora server. return super(MopidyPandoraAPIClient, self).get_station(station_id) - - -class PandoraResult(object): - - """Object for storing results of API calls for easy reference""" - - def __init__(self, result): - - self._raw_result = result - self.status_ok = False - self.message = "" - self.code = 0000 - - if str(result['stat']).upper() == 'OK': - self.status_ok = True - else: - self.status_ok = False - - try: - self.message = result['message'] - except KeyError as e: - if not self.status_ok: - raise e - - try: - self.code = result['code'] - except KeyError as e: - if not self.status_ok: - raise e diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index d8307fe..bcbc2be 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -2,7 +2,10 @@ import time -from mopidy_pandora.client import PandoraResult +from mopidy.internal import encoding + +from pandora.errors import PandoraException + from mopidy_pandora.library import PandoraUri logger = logging.getLogger(__name__) @@ -66,13 +69,16 @@ def process_click(self, method, track_uri): uri = PandoraUri.parse(track_uri) logger.info("Triggering event '%s' for song: %s", method, uri.name) + func = getattr(self, method) - result = PandoraResult(func(uri.token)) - if not result.status_ok: - logger.error('Error calling event: %s (code %s)', result.message, result.code) + try: + func(uri.token) + except PandoraException as e: + logger.error('Error calling event: %s', encoding.locale_decode(e)) + return False - return result.status_ok + return True def thumbs_up(self, track_token): return self.client.add_feedback(track_token, True) diff --git a/tests/test_client.py b/tests/test_client.py index 2fd954c..9576c16 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,8 +11,6 @@ import pytest -from mopidy_pandora.client import PandoraResult - from tests.conftest import get_backend from tests.conftest import get_station_list_mock @@ -93,37 +91,3 @@ def test_get_invalid_station(config): backend = get_backend(config) backend.api.get_station("9999999999999999999") - - -def test_pandora_result_success(): - - result = PandoraResult(conftest.success_result_mock()) - assert result.status_ok is True - - -def test_pandora_result_fail(): - - result = PandoraResult(conftest.fail_result_mock()) - assert result.status_ok is False - assert result.message == "An unexpected error occurred" - assert result.code == 9999 - - -def test_pandora_result_no_message_raises_exception(): - with pytest.raises(KeyError): - - inconsitent_result_mock_no_message = {"stat": "fail", - "code": 9999 - } - - PandoraResult(inconsitent_result_mock_no_message) - - -def test_pandora_result_no_code_raises_exception(): - with pytest.raises(KeyError): - - inconsitent_result_mock_no_code = {"stat": "fail", - "message": "An unexpected error occurred", - } - - PandoraResult(inconsitent_result_mock_no_code) diff --git a/tests/test_playback.py b/tests/test_playback.py index 015c778..60894e8 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -9,6 +9,8 @@ from mopidy import audio, backend as backend_api, models +from pandora.errors import PandoraException + from pandora.models.pandora import PlaylistItem, Station import pytest @@ -241,10 +243,10 @@ def set_event(): track_0 = TrackUri.from_track(playlist_item_mock, 0).uri track_1 = TrackUri.from_track(playlist_item_mock, 1).uri - process_click_mock = mock.PropertyMock() - process_click_mock.return_value = False + e = PandoraException().from_code(0000, "Mock exception") + provider._double_click_handler.thumbs_down = mock.Mock() + provider._double_click_handler.thumbs_down.side_effect = e - provider._double_click_handler.process_click = process_click_mock provider._double_click_handler.set_click_time() provider.active_track_uri = track_0 provider.change_track(models.Track(uri=track_1)) From 8ab9a7b692fa5056a845b47ecd80fef98563b0a7 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 28 Oct 2015 07:30:10 +0200 Subject: [PATCH 13/25] Ensure result of auto_setup tests are not affected by execution time. --- tests/test_playback.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_playback.py b/tests/test_playback.py index 60894e8..f00a035 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -283,13 +283,25 @@ def set_event(*args, **kwargs): values['set_single'].side_effect = set_event - provider.prepare_change() provider.prepare_change() - if event.wait(timeout=5.0): + if event.wait(timeout=1.0): values['set_repeat'].assert_called_once_with() values['set_random'].assert_called_once_with(False) values['set_consume'].assert_called_once_with(False) values['set_single'].assert_called_once_with(False) else: assert False + + event = threading.Event() + values['set_single'].side_effect = set_event + + provider.prepare_change() + + if event.wait(timeout=1.0): + assert False + else: + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) From 12b7f8364edb55a1f33e78542b81deb075f16add Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 28 Oct 2015 07:40:34 +0200 Subject: [PATCH 14/25] Remove outdated test fixtures. Update README. --- README.rst | 6 +++--- tests/conftest.py | 19 ------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index b3e76bc..6d923f2 100644 --- a/README.rst +++ b/README.rst @@ -111,9 +111,9 @@ v0.1.7 (UNRELEASED) ---------------------------------------- - Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file. -- Fix to resume playback after a track has been rated. -- Changed auto_setup routines to also ensure that 'consume', 'shuffle', and 'single' modes are disabled. -- Optimized auto_setup routines: now only called once, and only when the first pandora track starts to play. +- Now resumes playback after a track has been rated. +- Enhanced auto_setup routines to ensure that 'consume', 'shuffle', and 'single' modes are disabled as well. +- Optimized auto_setup routines: now only called when the Mopidy tracklist changes. v0.1.6 (Oct 26, 2015) ---------------------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index 719e067..c710e17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,25 +79,6 @@ def get_backend(config, simulate_request_exceptions=False): return obj -@pytest.fixture(scope="session") -def success_result_mock(): - mock_result = {"stat": "ok", - "result": {} - } - - return mock_result - - -@pytest.fixture(scope="session") -def fail_result_mock(): - mock_result = {"stat": "fail", - "message": "An unexpected error occurred", - "code": 9999 - } - - return mock_result - - @pytest.fixture(scope="session") def station_result_mock(): mock_result = {"stat": "ok", From f282d17a77e912138923f3562f302bdcc282c21a Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 29 Oct 2015 11:49:41 +0200 Subject: [PATCH 15/25] Refactoring: add constant for track skip limit. --- mopidy_pandora/playback.py | 8 ++++++-- tests/test_playback.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index 46a437a..f15d995 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -13,6 +13,8 @@ class PandoraPlaybackProvider(backend.PlaybackProvider): + SKIP_LIMIT = 3 + def __init__(self, audio, backend): super(PandoraPlaybackProvider, self).__init__(audio, backend) self._station = None @@ -47,6 +49,8 @@ def change_track(self, track): if track.uri is None: return False + track_uri = TrackUri.parse(track.uri) + station_id = PandoraUri.parse(track.uri).station_id if not self._station or station_id != self._station.id: @@ -54,7 +58,7 @@ def change_track(self, track): self._station_iter = iterate_forever(self._station.get_playlist) try: - next_track = self.get_next_track(TrackUri.parse(track.uri).index) + next_track = self.get_next_track(track_uri.index) if next_track: return super(PandoraPlaybackProvider, self).change_track(next_track) except requests.exceptions.RequestException as e: @@ -79,7 +83,7 @@ def get_next_track(self, index): else: consecutive_track_skips += 1 logger.warning("Track with uri '%s' is not playable.", TrackUri.from_track(track).uri) - if consecutive_track_skips >= 4: + if consecutive_track_skips >= self.SKIP_LIMIT: logger.error('Unplayable track skip limit exceeded!') return None diff --git a/tests/test_playback.py b/tests/test_playback.py index f00a035..df4751c 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -155,7 +155,7 @@ def test_change_track_enforces_skip_limit(provider): track = models.Track(uri="pandora:track:test::::") assert provider.change_track(track) is False - assert PlaylistItem.get_is_playable.call_count == 4 + assert PlaylistItem.get_is_playable.call_count == PandoraPlaybackProvider.SKIP_LIMIT def test_change_track_handles_request_exceptions(config, caplog): From 00aeebb2ddb39164a7bb28e8d089c4e46c185ca2 Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 29 Oct 2015 14:54:03 +0200 Subject: [PATCH 16/25] Fix incorrect reference in README to 'shuffle' mode instead of 'random'. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 6d923f2..b2fc805 100644 --- a/README.rst +++ b/README.rst @@ -86,7 +86,7 @@ Usage Mopidy needs `dynamic playlist `_ and `core extensions `_ support to properly support Pandora. In the meantime, Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode** -and **consume**, **shuffle**, and **single** should be turned off. Mopidy-Pandora will set this up automatically unless +and **consume**, **random**, and **single** should be turned off. Mopidy-Pandora will set this up automatically unless you set the **auto_setup** config parameter to 'false'. Each time a track is played, the next dynamic track for that Pandora station will be played. The playlist will consist @@ -112,7 +112,7 @@ v0.1.7 (UNRELEASED) - Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file. - Now resumes playback after a track has been rated. -- Enhanced auto_setup routines to ensure that 'consume', 'shuffle', and 'single' modes are disabled as well. +- Enhanced auto_setup routines to ensure that 'consume', 'random', and 'single' modes are disabled as well. - Optimized auto_setup routines: now only called when the Mopidy tracklist changes. v0.1.6 (Oct 26, 2015) From 4bc9217f15ee394332ab54efdb96ded056825c2b Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 29 Oct 2015 15:23:50 +0200 Subject: [PATCH 17/25] Add todo to handle shuffling of tracks. --- mopidy_pandora/doubleclick.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index bcbc2be..cfe5737 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -49,6 +49,8 @@ def on_change_track(self, active_track_uri, new_track_uri): new_track_index = int(PandoraUri.parse(new_track_uri).index) active_track_index = int(PandoraUri.parse(active_track_uri).index) + # TODO: the order of the tracks will no longer be sequential if the user has 'shuffled' the tracklist + # Need to find a better approach for determining whether 'next' or 'previous' was clicked. if new_track_index > active_track_index or new_track_index == 0 and active_track_index == 2: return self.process_click(self.on_pause_next_click, active_track_uri) From 4fa13c1e7f9af931406be33257265341c6d9306b Mon Sep 17 00:00:00 2001 From: jcass Date: Fri, 30 Oct 2015 16:09:10 +0200 Subject: [PATCH 18/25] Add py.test to testenv whitelist to suppress warning message for external command. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6d91e65..f597090 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py27, flake8 [testenv] sitepackages = true +whitelist_externals=py.test install_command = pip install --allow-unverified=mopidy --pre {opts} {packages} commands = py.test \ From 6764fa5c0473f39e0d5cb9eebd6ef59f574a343d Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 16:54:52 +0200 Subject: [PATCH 19/25] Fix superfluous retrieval of new station for every advertisement track. --- mopidy_pandora/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index f15d995..9918623 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -53,7 +53,7 @@ def change_track(self, track): station_id = PandoraUri.parse(track.uri).station_id - if not self._station or station_id != self._station.id: + if self._station is None or (station_id is not None and station_id != self._station.id): self._station = self.backend.api.get_station(station_id) self._station_iter = iterate_forever(self._station.get_playlist) From 50c0158136553775d7f5524160aa240b532a8f3d Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 17:55:50 +0200 Subject: [PATCH 20/25] Add reminder to skip retrieving new station list for ad tracks. --- mopidy_pandora/playback.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index 9918623..88d94ca 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -53,7 +53,9 @@ def change_track(self, track): station_id = PandoraUri.parse(track.uri).station_id - if self._station is None or (station_id is not None and station_id != self._station.id): + # TODO: should be able to perform check on is_ad() once dynamic tracklist support is available + # if not self._station or (not track.is_ad() and station_id != self._station.id): + if self._station is None or (station_id != '' and station_id != self._station.id): self._station = self.backend.api.get_station(station_id) self._station_iter = iterate_forever(self._station.get_playlist) From 025aba4b140b963c2fcc9e0e784ec348eac8a5a1 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 17:58:31 +0200 Subject: [PATCH 21/25] Fix naming of test source file. --- tests/{test_lookup.py => test_library.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_lookup.py => test_library.py} (100%) diff --git a/tests/test_lookup.py b/tests/test_library.py similarity index 100% rename from tests/test_lookup.py rename to tests/test_library.py From c471024894d0a20643c35ddd46c82474ceeb91db Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 18:10:13 +0200 Subject: [PATCH 22/25] Update version number. --- mopidy_pandora/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index d3ade1e..15a4870 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -9,7 +9,7 @@ from pandora import BaseAPIClient -__version__ = '0.1.6' +__version__ = '0.1.7' logger = logging.getLogger(__name__) From 4214ea18f0bbf95348dd7add7057efc6acc44f0b Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Wed, 26 Aug 2015 17:03:49 -0400 Subject: [PATCH 23/25] Set auto-repeat as soon as the first track in a new station starts to play. --- .travis.yml | 12 -- README.rst | 41 +++-- mopidy_pandora/__init__.py | 6 +- mopidy_pandora/backend.py | 11 +- mopidy_pandora/doubleclick.py | 63 +++++-- mopidy_pandora/ext.conf | 2 +- mopidy_pandora/playback.py | 55 ++++-- mopidy_pandora/rpc.py | 20 ++- tests/conftest.py | 23 ++- tests/test_backend.py | 11 ++ tests/test_doubleclick.py | 72 +++++--- tests/{test_lookup.py => test_library.py} | 0 tests/test_playback.py | 197 +++++++++++++++++----- tox.ini | 1 + 14 files changed, 382 insertions(+), 132 deletions(-) rename tests/{test_lookup.py => test_library.py} (100%) diff --git a/.travis.yml b/.travis.yml index a090495..0a0dd20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,12 @@ -sudo: false - language: python python: - "2.7_with_system_site_packages" -addons: - apt: - sources: - - mopidy-stable - packages: - - mopidy - env: - TOX_ENV=py27 - TOX_ENV=flake8 -install: - - "pip install tox" - script: - "tox -e $TOX_ENV" diff --git a/README.rst b/README.rst index d0aee42..b2fc805 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ Mopidy-Pandora to your Mopidy configuration file:: username = password = sort_order = date - auto_set_repeat = true + auto_setup = true ### EXPERIMENTAL EVENT HANDLING IMPLEMENTATION ### event_support_enabled = false @@ -74,9 +74,9 @@ alphabetical order. and web extensions: - double_click_interval - successive button clicks that occur within this interval (in seconds) will trigger the event. -- on_pause_resume_click - click pause and then play while a song is playing to trigger the event. Song continues afterwards. -- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :( -- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :( +- on_pause_resume_click - click pause and then play while a song is playing to trigger the event. +- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. +- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. The supported events are: thumbs_up, thumbs_down, sleep, add_artist_bookmark, add_song_bookmark @@ -85,9 +85,9 @@ Usage Mopidy needs `dynamic playlist `_ and `core extensions `_ support to properly support Pandora. In the meantime, -Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode**, -and Mopidy-Pandora will enable this automatically just before each track is changed unless you set the **auto_set_repeat** -config parameter to 'false'. +Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode** +and **consume**, **random**, and **single** should be turned off. Mopidy-Pandora will set this up automatically unless +you set the **auto_setup** config parameter to 'false'. Each time a track is played, the next dynamic track for that Pandora station will be played. The playlist will consist of a single track unless the experimental ratings support is enabled. With ratings support enabled, the playlist will @@ -107,24 +107,37 @@ Project resources Changelog ========= -v0.1.5 (UNRELEASED) +v0.1.7 (UNRELEASED) ---------------------------------------- -- Add option to automatically set tracks to play in repeat mode, using Mopidy's 'about-to-finish' callback. +- Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file. +- Now resumes playback after a track has been rated. +- Enhanced auto_setup routines to ensure that 'consume', 'random', and 'single' modes are disabled as well. +- Optimized auto_setup routines: now only called when the Mopidy tracklist changes. + +v0.1.6 (Oct 26, 2015) +---------------------------------------- + +- Release to pypi + +v0.1.5 (Aug 20, 2015) +---------------------------------------- + +- Add option to automatically set tracks to play in repeat mode when Mopidy-Pandora starts. - Add experimental support for rating songs by re-using buttons available in the current front-end Mopidy extensions. - Audio quality now defaults to the highest setting. - Improved caching to revert to Pandora server if station cannot be found in the local cache. - Fix to retrieve stations by ID instead of token. - Add unit tests to increase test coverage. -v0.1.4 (UNRELEASED) +v0.1.4 (Aug 17, 2015) ---------------------------------------- - Limit number of consecutive track skips to prevent Mopidy's skip-to-next-on-error behaviour from locking the user's Pandora account. - Better handling of exceptions that occur in the backend to prevent Mopidy actor crashes. - Add support for unicode characters in station and track names. -v0.1.3 (UNRELEASED) +v0.1.3 (Jul 11, 2015) ---------------------------------------- - Update to work with release of Mopidy version 1.0 @@ -133,19 +146,19 @@ v0.1.3 (UNRELEASED) - Get rid of 'Stations' root directory. Browsing now displays all of the available stations immediately. - Fill artist name to improve how tracks are displayed in various Mopidy front-end extensions. -v0.1.2 (UNRELEASED) +v0.1.2 (Jun 20, 2015) ---------------------------------------- - Enhancement to handle 'Invalid Auth Token' exceptions when the Pandora session expires after long periods of inactivity. Allows Mopidy-Pandora to run indefinitely on dedicated music servers like the Pi MusicBox. - Add configuration option to sort stations alphabetically, instead of by date. -v0.1.1 (UNRELEASED) +v0.1.1 (Mar 22, 2015) ---------------------------------------- - Added ability to make preferred audio quality user-configurable. -v0.1.0 (UNRELEASED) +v0.1.0 (Dec 28, 2014) ---------------------------------------- - Initial release. diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index fbc27b7..15a4870 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -4,11 +4,12 @@ import os from mopidy import config, ext +from mopidy.config import Deprecated from pandora import BaseAPIClient -__version__ = '0.1.5' +__version__ = '0.1.7' logger = logging.getLogger(__name__) @@ -37,7 +38,8 @@ def get_config_schema(self): BaseAPIClient.MED_AUDIO_QUALITY, BaseAPIClient.HIGH_AUDIO_QUALITY]) schema['sort_order'] = config.String(optional=True, choices=['date', 'A-Z', 'a-z']) - schema['auto_set_repeat'] = config.Boolean() + schema['auto_setup'] = config.Boolean() + schema['auto_set_repeat'] = Deprecated() schema['event_support_enabled'] = config.Boolean() schema['double_click_interval'] = config.String() schema['on_pause_resume_click'] = config.String(choices=['thumbs_up', 'thumbs_down', 'sleep']) diff --git a/mopidy_pandora/backend.py b/mopidy_pandora/backend.py index 54efcff..1fa60bb 100644 --- a/mopidy_pandora/backend.py +++ b/mopidy_pandora/backend.py @@ -1,4 +1,4 @@ -from mopidy import backend +from mopidy import backend, core from mopidy.internal import encoding from pandora import BaseAPIClient, clientbuilder @@ -15,7 +15,7 @@ from mopidy_pandora.uri import logger -class PandoraBackend(pykka.ThreadingActor, backend.Backend): +class PandoraBackend(pykka.ThreadingActor, backend.Backend, core.CoreListener): def __init__(self, config, audio): super(PandoraBackend, self).__init__() @@ -33,7 +33,9 @@ def __init__(self, config, audio): self.library = PandoraLibraryProvider(backend=self, sort_order=self._config['sort_order']) - self.auto_set_repeat = self._config['auto_set_repeat'] + self.auto_setup = self._config['auto_setup'] + self.setup_required = self.auto_setup + self.rpc_client = rpc.RPCClient(config['http']['hostname'], config['http']['port']) self.supports_events = False @@ -50,3 +52,6 @@ def on_start(self): self.api.login(self._config["username"], self._config["password"]) except requests.exceptions.RequestException as e: logger.error('Error logging in to Pandora: %s', encoding.locale_decode(e)) + + def tracklist_changed(self): + self.setup_required = self.auto_setup diff --git a/mopidy_pandora/doubleclick.py b/mopidy_pandora/doubleclick.py index 80df722..cfe5737 100644 --- a/mopidy_pandora/doubleclick.py +++ b/mopidy_pandora/doubleclick.py @@ -2,6 +2,10 @@ import time +from mopidy.internal import encoding + +from pandora.errors import PandoraException + from mopidy_pandora.library import PandoraUri logger = logging.getLogger(__name__) @@ -14,56 +18,81 @@ def __init__(self, config, client): self.on_pause_previous_click = config["on_pause_previous_click"] self.double_click_interval = config['double_click_interval'] self.client = client - self.click_time = 0 + self._click_time = 0 + + def set_click_time(self, click_time=None): + if click_time is None: + self._click_time = time.time() + else: + self._click_time = click_time - def set_click(self): - self.click_time = time.time() + def get_click_time(self): + return self._click_time def is_double_click(self): - return time.time() - self.click_time < float(self.double_click_interval) + + double_clicked = self._click_time > 0 and time.time() - self._click_time < float(self.double_click_interval) + + if double_clicked is False: + self._click_time = 0 + + return double_clicked def on_change_track(self, active_track_uri, new_track_uri): from mopidy_pandora.uri import PandoraUri if not self.is_double_click(): - return + return False - # TODO: Won't work if 'shuffle' or 'consume' modes are enabled if active_track_uri is not None: new_track_index = int(PandoraUri.parse(new_track_uri).index) active_track_index = int(PandoraUri.parse(active_track_uri).index) + # TODO: the order of the tracks will no longer be sequential if the user has 'shuffled' the tracklist + # Need to find a better approach for determining whether 'next' or 'previous' was clicked. if new_track_index > active_track_index or new_track_index == 0 and active_track_index == 2: - self.process_click(self.on_pause_next_click, active_track_uri) + return self.process_click(self.on_pause_next_click, active_track_uri) elif new_track_index < active_track_index or new_track_index == active_track_index: - self.process_click(self.on_pause_previous_click, active_track_uri) + return self.process_click(self.on_pause_previous_click, active_track_uri) + + return False def on_resume_click(self, track_uri, time_position): if not self.is_double_click() or time_position == 0: - return + return False - self.process_click(self.on_pause_resume_click, track_uri) + return self.process_click(self.on_pause_resume_click, track_uri) def process_click(self, method, track_uri): + + self.set_click_time(0) + uri = PandoraUri.parse(track_uri) logger.info("Triggering event '%s' for song: %s", method, uri.name) + func = getattr(self, method) - func(uri.token) - self.click_time = 0 + + try: + func(uri.token) + except PandoraException as e: + logger.error('Error calling event: %s', encoding.locale_decode(e)) + return False + + return True def thumbs_up(self, track_token): - self.client.add_feedback(track_token, True) + return self.client.add_feedback(track_token, True) def thumbs_down(self, track_token): - self.client.add_feedback(track_token, False) + return self.client.add_feedback(track_token, False) def sleep(self, track_token): - self.client.sleep_song(track_token) + return self.client.sleep_song(track_token) def add_artist_bookmark(self, track_token): - self.client.add_artist_bookmark(track_token) + return self.client.add_artist_bookmark(track_token) def add_song_bookmark(self, track_token): - self.client.add_song_bookmark(track_token) + return self.client.add_song_bookmark(track_token) diff --git a/mopidy_pandora/ext.conf b/mopidy_pandora/ext.conf index 832a238..c125607 100644 --- a/mopidy_pandora/ext.conf +++ b/mopidy_pandora/ext.conf @@ -10,7 +10,7 @@ username = password = preferred_audio_quality = highQuality sort_order = date -auto_set_repeat = true +auto_setup = true ### EXPERIMENTAL RATINGS IMPLEMENTATION ### event_support_enabled = false diff --git a/mopidy_pandora/playback.py b/mopidy_pandora/playback.py index 844fef5..88d94ca 100644 --- a/mopidy_pandora/playback.py +++ b/mopidy_pandora/playback.py @@ -1,3 +1,5 @@ +from threading import Thread + from mopidy import backend, models from mopidy.internal import encoding @@ -11,38 +13,54 @@ class PandoraPlaybackProvider(backend.PlaybackProvider): + SKIP_LIMIT = 3 + def __init__(self, audio, backend): super(PandoraPlaybackProvider, self).__init__(audio, backend) self._station = None self._station_iter = None self.active_track_uri = None - if self.backend.auto_set_repeat: - # Make sure that tracks are being played in 'repeat mode'. - self.audio.set_about_to_finish_callback(self.callback).get() - - def callback(self): # TODO: add gapless playback when it is supported in Mopidy > 1.1 + # self.audio.set_about_to_finish_callback(self.callback).get() + + # def callback(self): # See: https://discuss.mopidy.com/t/has-the-gapless-playback-implementation-been-completed-yet/784/2 # self.audio.set_uri(self.translate_uri(self.get_next_track())).get() - uri = self.backend.rpc_client.get_current_track_uri() - if uri is not None and uri.startswith("pandora:"): - self.backend.rpc_client.set_repeat() + def _auto_setup(self): + + self.backend.rpc_client.set_repeat() + self.backend.rpc_client.set_consume(False) + self.backend.rpc_client.set_random(False) + self.backend.rpc_client.set_single(False) + + self.backend.setup_required = False + + def prepare_change(self): + + if self.backend.auto_setup and self.backend.setup_required: + Thread(target=self._auto_setup).start() + + super(PandoraPlaybackProvider, self).prepare_change() def change_track(self, track): if track.uri is None: return False + track_uri = TrackUri.parse(track.uri) + station_id = PandoraUri.parse(track.uri).station_id - if not self._station or station_id != self._station.id: + # TODO: should be able to perform check on is_ad() once dynamic tracklist support is available + # if not self._station or (not track.is_ad() and station_id != self._station.id): + if self._station is None or (station_id != '' and station_id != self._station.id): self._station = self.backend.api.get_station(station_id) self._station_iter = iterate_forever(self._station.get_playlist) try: - next_track = self.get_next_track(TrackUri.parse(track.uri).index) + next_track = self.get_next_track(track_uri.index) if next_track: return super(PandoraPlaybackProvider, self).change_track(next_track) except requests.exceptions.RequestException as e: @@ -67,7 +85,7 @@ def get_next_track(self, index): else: consecutive_track_skips += 1 logger.warning("Track with uri '%s' is not playable.", TrackUri.from_track(track).uri) - if consecutive_track_skips >= 4: + if consecutive_track_skips >= self.SKIP_LIMIT: logger.error('Unplayable track skip limit exceeded!') return None @@ -84,13 +102,22 @@ def __init__(self, audio, backend): def change_track(self, track): - self._double_click_handler.on_change_track(self.active_track_uri, track.uri) - return super(EventSupportPlaybackProvider, self).change_track(track) + event_processed = self._double_click_handler.on_change_track(self.active_track_uri, track.uri) + return_value = super(EventSupportPlaybackProvider, self).change_track(track) + + if event_processed: + Thread(target=self.backend.rpc_client.resume_playback).start() + + return return_value def pause(self): - self._double_click_handler.set_click() + + if self.get_time_position() > 0: + self._double_click_handler.set_click_time() + return super(EventSupportPlaybackProvider, self).pause() def resume(self): self._double_click_handler.on_resume_click(self.active_track_uri, self.get_time_position()) + return super(EventSupportPlaybackProvider, self).resume() diff --git a/mopidy_pandora/rpc.py b/mopidy_pandora/rpc.py index cc62e11..2a1b728 100644 --- a/mopidy_pandora/rpc.py +++ b/mopidy_pandora/rpc.py @@ -18,11 +18,27 @@ def _do_rpc(self, method, params=None): return requests.request('POST', self.url, data=json.dumps(data), headers={'Content-Type': 'application/json'}) - def set_repeat(self): + def set_repeat(self, value=True): - self._do_rpc('core.tracklist.set_repeat', {'value': True}) + self._do_rpc('core.tracklist.set_repeat', {'value': value}) + + def set_consume(self, value=True): + + self._do_rpc('core.tracklist.set_consume', {'value': value}) + + def set_single(self, value=True): + + self._do_rpc('core.tracklist.set_single', {'value': value}) + + def set_random(self, value=True): + + self._do_rpc('core.tracklist.set_random', {'value': value}) def get_current_track_uri(self): response = self._do_rpc('core.playback.get_current_tl_track') return response.json()['result']['track']['uri'] + + def resume_playback(self): + + self._do_rpc('core.playback.resume') diff --git a/tests/conftest.py b/tests/conftest.py index da5c4d6..c710e17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ def config(): 'password': 'doe', 'preferred_audio_quality': MOCK_DEFAULT_AUDIO_QUALITY, 'sort_order': 'date', - 'auto_set_repeat': True, + 'auto_setup': True, 'event_support_enabled': True, 'double_click_interval': '0.1', @@ -66,6 +66,8 @@ def config(): def get_backend(config, simulate_request_exceptions=False): obj = backend.PandoraBackend(config=config, audio=Mock()) + obj.rpc_client._do_rpc = rpc_call_not_implemented_mock + if simulate_request_exceptions: type(obj.api.transport).__call__ = request_exception_mock else: @@ -88,12 +90,12 @@ def station_result_mock(): "stationName": MOCK_STATION_NAME}, } - return mock_result["result"] + return mock_result @pytest.fixture(scope="session") def station_mock(simulate_request_exceptions=False): - return Station.from_json(get_backend(config(), simulate_request_exceptions).api, station_result_mock()) + return Station.from_json(get_backend(config(), simulate_request_exceptions).api, station_result_mock()["result"]) @pytest.fixture(scope="session") @@ -136,12 +138,12 @@ def playlist_result_mock(): "stationId": MOCK_STATION_ID, "songRating": 0, }]}} - return mock_result["result"] + return mock_result @pytest.fixture(scope="session") def playlist_mock(simulate_request_exceptions=False): - return Playlist.from_json(get_backend(config(), simulate_request_exceptions).api, playlist_result_mock()) + return Playlist.from_json(get_backend(config(), simulate_request_exceptions).api, playlist_result_mock()["result"]) @pytest.fixture(scope="session") @@ -157,7 +159,7 @@ def get_station_playlist_mock(self): @pytest.fixture(scope="session") def playlist_item_mock(): return PlaylistItem.from_json(get_backend( - config()).api, playlist_result_mock()["items"][0]) + config()).api, playlist_result_mock()["result"]["items"][0]) @pytest.fixture(scope="session") @@ -191,3 +193,12 @@ def transport_call_not_implemented_mock(self, method, **data): class TransportCallTestNotImplemented(Exception): pass + + +@pytest.fixture +def rpc_call_not_implemented_mock(method, params=None): + raise RPCCallTestNotImplemented(method + "(" + params + ")") + + +class RPCCallTestNotImplemented(Exception): + pass diff --git a/tests/test_backend.py b/tests/test_backend.py index 4479416..526a170 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -74,3 +74,14 @@ def test_on_start_handles_request_exception(config, caplog): # Check that request exceptions are caught and logged assert 'Error logging in to Pandora' in caplog.text() + + +def test_auto_setup_resets_when_tracklist_changes(config): + + backend = get_backend(config, True) + + backend.setup_required = False + + backend.tracklist_changed() + + assert backend.setup_required diff --git a/tests/test_doubleclick.py b/tests/test_doubleclick.py index ac27d25..a508221 100644 --- a/tests/test_doubleclick.py +++ b/tests/test_doubleclick.py @@ -11,6 +11,7 @@ from mopidy_pandora.backend import MopidyPandoraAPIClient from mopidy_pandora.doubleclick import DoubleClickHandler +from mopidy_pandora.playback import PandoraPlaybackProvider from mopidy_pandora.uri import PandoraUri, TrackUri @@ -29,7 +30,8 @@ def handler(config): sleep_mock = mock.PropertyMock() handler.client.sleep_song = sleep_mock - handler.set_click() + handler.set_click_time() + return handler @@ -41,20 +43,14 @@ def test_is_double_click(handler): assert handler.is_double_click() is False -def test_no_duplicate_triggers(handler, playlist_item_mock): +def test_is_double_click_resets_click_time(handler): assert handler.is_double_click() - thumbs_up_mock = mock.PropertyMock() - handler.thumbs_up = thumbs_up_mock - - track_uri = TrackUri.from_track(playlist_item_mock).uri - handler.on_resume_click(track_uri, 100) - + time.sleep(float(handler.double_click_interval) + 0.1) assert handler.is_double_click() is False - handler.on_resume_click(track_uri, 100) - handler.thumbs_up.assert_called_once_with(PandoraUri.parse(track_uri).token) + assert handler.get_click_time() == 0 def test_on_change_track_forward(config, handler, playlist_item_mock): @@ -101,35 +97,69 @@ def test_on_resume_click_ignored_if_start_of_track(handler, playlist_item_mock): def test_on_resume_click(config, handler, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): - process_click_mock = mock.PropertyMock() - handler.process_click = process_click_mock + process_click_mock = mock.PropertyMock() + handler.process_click = process_click_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + handler.on_resume_click(track_uri, 100) + + handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], track_uri) + + +def test_process_click_resets_click_time(config, handler, playlist_item_mock): + + thumbs_up_mock = mock.PropertyMock() + + handler.thumbs_up = thumbs_up_mock track_uri = TrackUri.from_track(playlist_item_mock).uri - handler.on_resume_click(track_uri, 100) - handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], track_uri) + handler.process_click(config['pandora']['on_pause_resume_click'], track_uri) + + assert handler.get_click_time() == 0 -def test_process_click(config, handler, playlist_item_mock): +def test_process_click_resume(config, handler, playlist_item_mock): thumbs_up_mock = mock.PropertyMock() - thumbs_down_mock = mock.PropertyMock() - sleep_mock = mock.PropertyMock() handler.thumbs_up = thumbs_up_mock - handler.thumbs_down = thumbs_down_mock - handler.sleep = sleep_mock track_uri = TrackUri.from_track(playlist_item_mock).uri handler.process_click(config['pandora']['on_pause_resume_click'], track_uri) - handler.process_click(config['pandora']['on_pause_next_click'], track_uri) - handler.process_click(config['pandora']['on_pause_previous_click'], track_uri) token = PandoraUri.parse(track_uri).token handler.thumbs_up.assert_called_once_with(token) + + +def test_process_click_next(config, handler, playlist_item_mock): + + thumbs_down_mock = mock.PropertyMock() + + handler.thumbs_down = thumbs_down_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + + handler.process_click(config['pandora']['on_pause_next_click'], track_uri) + + token = PandoraUri.parse(track_uri).token handler.thumbs_down.assert_called_once_with(token) + + +def test_process_click_previous(config, handler, playlist_item_mock): + + sleep_mock = mock.PropertyMock() + + handler.sleep = sleep_mock + + track_uri = TrackUri.from_track(playlist_item_mock).uri + + handler.process_click(config['pandora']['on_pause_previous_click'], track_uri) + + token = PandoraUri.parse(track_uri).token handler.sleep.assert_called_once_with(token) diff --git a/tests/test_lookup.py b/tests/test_library.py similarity index 100% rename from tests/test_lookup.py rename to tests/test_library.py diff --git a/tests/test_playback.py b/tests/test_playback.py index f3d853b..df4751c 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -1,11 +1,16 @@ from __future__ import unicode_literals +import threading +import time + import conftest import mock from mopidy import audio, backend as backend_api, models +from pandora.errors import PandoraException + from pandora.models.pandora import PlaylistItem, Station import pytest @@ -47,33 +52,40 @@ def test_change_track_aborts_if_no_track_uri(provider): def test_pause_starts_double_click_timer(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): + assert provider.backend.supports_events + assert provider._double_click_handler.get_click_time() == 0 + provider.pause() + assert provider._double_click_handler.get_click_time() > 0 - assert provider.backend.supports_events - assert provider._double_click_handler.click_time == 0 - provider.pause() - assert provider._double_click_handler.click_time > 0 +def test_pause_does_not_start_timer_at_track_start(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=0): + assert provider.backend.supports_events + assert provider._double_click_handler.get_click_time() == 0 + provider.pause() + assert provider._double_click_handler.get_click_time() == 0 -def test_resume_checks_for_double_click(provider): - assert provider.backend.supports_events - is_double_click_mock = mock.PropertyMock() - process_click_mock = mock.PropertyMock() - provider._double_click_handler.is_double_click = is_double_click_mock - provider._double_click_handler.process_click = process_click_mock - provider.resume() +def test_resume_checks_for_double_click(provider): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): + assert provider.backend.supports_events + is_double_click_mock = mock.PropertyMock() + process_click_mock = mock.PropertyMock() + provider._double_click_handler.is_double_click = is_double_click_mock + provider._double_click_handler.process_click = process_click_mock + provider.resume() - provider._double_click_handler.is_double_click.assert_called_once_with() + provider._double_click_handler.is_double_click.assert_called_once_with() def test_resume_double_click_call(config, provider): - assert provider.backend.supports_events process_click_mock = mock.PropertyMock() provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click() + provider._double_click_handler.set_click_time() provider.resume() provider._double_click_handler.process_click.assert_called_once_with(config['pandora']['on_pause_resume_click'], @@ -82,15 +94,16 @@ def test_resume_double_click_call(config, provider): def test_change_track_checks_for_double_click(provider): with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(PandoraPlaybackProvider, 'get_time_position', return_value=100): + assert provider.backend.supports_events + is_double_click_mock = mock.PropertyMock() + process_click_mock = mock.PropertyMock() + provider._double_click_handler.is_double_click = is_double_click_mock + provider._double_click_handler.process_click = process_click_mock + provider.backend.rpc_client.resume_playback = mock.PropertyMock() + provider.change_track(models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri)) - assert provider.backend.supports_events - is_double_click_mock = mock.PropertyMock() - process_click_mock = mock.PropertyMock() - provider._double_click_handler.is_double_click = is_double_click_mock - provider._double_click_handler.process_click = process_click_mock - provider.change_track(models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri)) - - provider._double_click_handler.is_double_click.assert_called_once_with() + provider._double_click_handler.is_double_click.assert_called_once_with() def test_change_track_double_click_call(config, provider, playlist_item_mock): @@ -104,13 +117,16 @@ def test_change_track_double_click_call(config, provider, playlist_item_mock): process_click_mock = mock.PropertyMock() provider._double_click_handler.process_click = process_click_mock - provider._double_click_handler.set_click() + provider.backend.rpc_client.resume_playback = mock.PropertyMock() + provider._double_click_handler.set_click_time() provider.active_track_uri = track_0 provider.change_track(models.Track(uri=track_1)) provider._double_click_handler.process_click.assert_called_once_with(config['pandora']['on_pause_next_click'], provider.active_track_uri) + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_1 provider.change_track(models.Track(uri=track_0)) @@ -122,14 +138,13 @@ def test_change_track(audio_mock, provider): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', return_value=True): - track = models.Track(uri=TrackUri.from_track(conftest.playlist_item_mock()).uri) assert provider.change_track(track) is True assert audio_mock.prepare_change.call_count == 0 assert audio_mock.start_playback.call_count == 0 audio_mock.set_uri.assert_called_once_with(PlaylistItem.get_audio_url( - conftest.playlist_result_mock()["items"][0], + conftest.playlist_result_mock()["result"]["items"][0], conftest.MOCK_DEFAULT_AUDIO_QUALITY)) @@ -137,17 +152,15 @@ def test_change_track_enforces_skip_limit(provider): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', return_value=False): - track = models.Track(uri="pandora:track:test::::") assert provider.change_track(track) is False - assert PlaylistItem.get_is_playable.call_count == 4 + assert PlaylistItem.get_is_playable.call_count == PandoraPlaybackProvider.SKIP_LIMIT def test_change_track_handles_request_exceptions(config, caplog): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.request_exception_mock): - track = models.Track(uri="pandora:track:test::::") playback = conftest.get_backend(config).playback @@ -156,11 +169,98 @@ def test_change_track_handles_request_exceptions(config, caplog): assert 'Error changing track' in caplog.text() +def test_change_track_resumes_playback(provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events + + event = threading.Event() + + def set_event(): + event.set() + + mock_rpc.side_effect = set_event + + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + process_click_mock = mock.PropertyMock() + + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + + provider.change_track(models.Track(uri=track_1)) + + if event.wait(timeout=1.0): + mock_rpc.assert_called_once_with() + else: + assert False + + +def test_change_track_does_not_resume_playback_if_not_doubleclick(config, provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events + + event = threading.Event() + + def set_event(): + event.set() + + mock_rpc.side_effect = set_event + + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + process_click_mock = mock.PropertyMock() + + click_interval = float(config['pandora']['double_click_interval']) + 1.0 + + provider._double_click_handler.process_click = process_click_mock + provider._double_click_handler.set_click_time(time.time() - click_interval) + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) + + if event.wait(timeout=1.0): + assert False + else: + assert not mock_rpc.called + + +def test_change_track_does_not_resume_playback_if_event_failed(provider, playlist_item_mock): + with mock.patch.object(PandoraPlaybackProvider, 'change_track', return_value=True): + with mock.patch.object(RPCClient, 'resume_playback') as mock_rpc: + assert provider.backend.supports_events + + event = threading.Event() + + def set_event(): + event.set() + + mock_rpc.side_effect = set_event + + track_0 = TrackUri.from_track(playlist_item_mock, 0).uri + track_1 = TrackUri.from_track(playlist_item_mock, 1).uri + + e = PandoraException().from_code(0000, "Mock exception") + provider._double_click_handler.thumbs_down = mock.Mock() + provider._double_click_handler.thumbs_down.side_effect = e + + provider._double_click_handler.set_click_time() + provider.active_track_uri = track_0 + provider.change_track(models.Track(uri=track_1)) + + if event.wait(timeout=1.0): + assert False + else: + assert not mock_rpc.called + + def test_is_playable_handles_request_exceptions(provider, caplog): with mock.patch.object(MopidyPandoraAPIClient, 'get_station', conftest.get_station_mock): with mock.patch.object(Station, 'get_playlist', conftest.get_station_playlist_mock): with mock.patch.object(PlaylistItem, 'get_is_playable', conftest.request_exception_mock): - track = models.Track(uri="pandora:track:test::::") assert provider.change_track(track) is False @@ -168,23 +268,40 @@ def test_is_playable_handles_request_exceptions(provider, caplog): def test_translate_uri_returns_audio_url(provider): - assert provider.translate_uri("pandora:track:test:::::audio_url") == "audio_url" -def test_auto_set_repeat_off_for_non_pandora_uri(provider): - with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="not_a_pandora_uri::::::"): +def test_auto_setup_only_called_once(provider): + with mock.patch.multiple('mopidy_pandora.rpc.RPCClient', set_repeat=mock.DEFAULT, set_random=mock.DEFAULT, + set_consume=mock.DEFAULT, set_single=mock.DEFAULT) as values: + with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): - provider.callback() + event = threading.Event() - assert not provider.backend.rpc_client.set_repeat.called + def set_event(*args, **kwargs): + event.set() + values['set_single'].side_effect = set_event -def test_auto_set_repeat_on_for_pandora_uri(provider): - with mock.patch.object(RPCClient, 'set_repeat', mock.Mock()): - with mock.patch.object(RPCClient, 'get_current_track_uri', return_value="pandora::::::"): + provider.prepare_change() + + if event.wait(timeout=1.0): + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) + else: + assert False + + event = threading.Event() + values['set_single'].side_effect = set_event - provider.callback() + provider.prepare_change() - provider.backend.rpc_client.set_repeat.assert_called_once_with() + if event.wait(timeout=1.0): + assert False + else: + values['set_repeat'].assert_called_once_with() + values['set_random'].assert_called_once_with(False) + values['set_consume'].assert_called_once_with(False) + values['set_single'].assert_called_once_with(False) diff --git a/tox.ini b/tox.ini index 6d91e65..f597090 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py27, flake8 [testenv] sitepackages = true +whitelist_externals=py.test install_command = pip install --allow-unverified=mopidy --pre {opts} {packages} commands = py.test \ From 62b2c9d74ea3c4cec228859db64f59db085d4177 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 18:39:47 +0200 Subject: [PATCH 24/25] Sync travis config. --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0a0dd20..a090495 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,24 @@ +sudo: false + language: python python: - "2.7_with_system_site_packages" +addons: + apt: + sources: + - mopidy-stable + packages: + - mopidy + env: - TOX_ENV=py27 - TOX_ENV=flake8 +install: + - "pip install tox" + script: - "tox -e $TOX_ENV" From 04c6ea8e2579dc56c3906a1ae79072afc2a0fd7f Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 31 Oct 2015 18:43:48 +0200 Subject: [PATCH 25/25] Add release date to README. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b2fc805..c746651 100644 --- a/README.rst +++ b/README.rst @@ -107,7 +107,7 @@ Project resources Changelog ========= -v0.1.7 (UNRELEASED) +v0.1.7 (Oct 31, 2015) ---------------------------------------- - Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file.