diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 74f4bebd..0d0875f0 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -90,7 +90,7 @@ class PlaybackController(object): return None uri = self.current_cp_track.track.uri uri_scheme = urlparse.urlparse(uri).scheme - return self.backends.by_uri_scheme[uri_scheme] + return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None) def _get_cpid(self, cp_track): if cp_track is None: @@ -298,9 +298,10 @@ class PlaybackController(object): def time_position(self): """Time position in milliseconds.""" backend = self._get_backend() - if backend is None: + if backend: + return backend.playback.get_time_position().get() + else: return 0 - return backend.playback.get_time_position().get() @property def volume(self): @@ -387,7 +388,7 @@ class PlaybackController(object): def pause(self): """Pause playback.""" backend = self._get_backend() - if backend is None or backend.playback.pause().get(): + if not backend or backend.playback.pause().get(): self.state = PlaybackState.PAUSED self._trigger_track_playback_paused() @@ -419,7 +420,8 @@ class PlaybackController(object): if cp_track is not None: self.current_cp_track = cp_track self.state = PlaybackState.PLAYING - if not self._get_backend().playback.play(cp_track.track).get(): + backend = self._get_backend() + if not backend or not backend.playback.play(cp_track.track).get(): # Track is not playable if self.random and self._shuffled: self._shuffled.remove(cp_track) @@ -445,8 +447,10 @@ class PlaybackController(object): def resume(self): """If paused, resume playing the current track.""" - if (self.state == PlaybackState.PAUSED and - self._get_backend().playback.resume().get()): + if self.state != PlaybackState.PAUSED: + return + backend = self._get_backend() + if backend and backend.playback.resume().get(): self.state = PlaybackState.PLAYING self._trigger_track_playback_resumed() @@ -472,7 +476,11 @@ class PlaybackController(object): self.next() return True - success = self._get_backend().playback.seek(time_position).get() + backend = self._get_backend() + if not backend: + return False + + success = backend.playback.seek(time_position).get() if success: self._trigger_seeked(time_position) return success @@ -486,7 +494,8 @@ class PlaybackController(object): :type clear_current_track: boolean """ if self.state != PlaybackState.STOPPED: - if self._get_backend().playback.stop().get(): + backend = self._get_backend() + if not backend or backend.playback.stop().get(): self._trigger_track_playback_ended() self.state = PlaybackState.STOPPED if clear_current_track: diff --git a/tests/core/playback_test.py b/tests/core/playback_test.py index b3a75773..e67aa0c3 100644 --- a/tests/core/playback_test.py +++ b/tests/core/playback_test.py @@ -1,7 +1,7 @@ import mock from mopidy.backends import base -from mopidy.core import Core +from mopidy.core import Core, PlaybackState from mopidy.models import Track from tests import unittest @@ -19,17 +19,24 @@ class CorePlaybackTest(unittest.TestCase): self.playback2 = mock.Mock(spec=base.BasePlaybackProvider) self.backend2.playback = self.playback2 + # A backend without the optional playback provider + self.backend3 = mock.Mock() + self.backend3.uri_schemes.get.return_value = ['dummy3'] + self.backend3.has_playback().get.return_value = False + self.tracks = [ - Track(uri='dummy1://foo', length=40000), - Track(uri='dummy1://bar', length=40000), - Track(uri='dummy2://foo', length=40000), - Track(uri='dummy2://bar', length=40000), + Track(uri='dummy1:a', length=40000), + Track(uri='dummy2:a', length=40000), + Track(uri='dummy3:a', length=40000), # Unplayable + Track(uri='dummy1:b', length=40000), ] - self.core = Core(audio=None, backends=[self.backend1, self.backend2]) + self.core = Core(audio=None, backends=[ + self.backend1, self.backend2, self.backend3]) self.core.current_playlist.append(self.tracks) self.cp_tracks = self.core.current_playlist.cp_tracks + self.unplayable_cp_track = self.cp_tracks[2] def test_play_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) @@ -38,10 +45,19 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.play.called) def test_play_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.assertFalse(self.playback1.play.called) - self.playback2.play.assert_called_once_with(self.tracks[2]) + self.playback2.play.assert_called_once_with(self.tracks[1]) + + def test_play_skips_to_next_on_unplayable_track(self): + self.core.playback.play(self.unplayable_cp_track) + + self.playback1.play.assert_called_once_with(self.tracks[3]) + self.assertFalse(self.playback2.play.called) + + self.assertEqual(self.core.playback.current_cp_track, + self.cp_tracks[3]) def test_pause_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) @@ -51,12 +67,20 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.pause.called) def test_pause_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.core.playback.pause() self.assertFalse(self.playback1.pause.called) self.playback2.pause.assert_called_once_with() + def test_pause_changes_state_even_if_track_is_unplayable(self): + self.core.playback.current_cp_track = self.unplayable_cp_track + self.core.playback.pause() + + self.assertEqual(self.core.playback.state, PlaybackState.PAUSED) + self.assertFalse(self.playback1.pause.called) + self.assertFalse(self.playback2.pause.called) + def test_resume_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) self.core.playback.pause() @@ -66,13 +90,22 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.resume.called) def test_resume_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.core.playback.pause() self.core.playback.resume() self.assertFalse(self.playback1.resume.called) self.playback2.resume.assert_called_once_with() + def test_resume_does_nothing_if_track_is_unplayable(self): + self.core.playback.current_cp_track = self.unplayable_cp_track + self.core.playback.state = PlaybackState.PAUSED + self.core.playback.resume() + + self.assertEqual(self.core.playback.state, PlaybackState.PAUSED) + self.assertFalse(self.playback1.resume.called) + self.assertFalse(self.playback2.resume.called) + def test_stop_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) self.core.playback.stop() @@ -81,12 +114,21 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.stop.called) def test_stop_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.core.playback.stop() self.assertFalse(self.playback1.stop.called) self.playback2.stop.assert_called_once_with() + def test_stop_changes_state_even_if_track_is_unplayable(self): + self.core.playback.current_cp_track = self.unplayable_cp_track + self.core.playback.state = PlaybackState.PAUSED + self.core.playback.stop() + + self.assertEqual(self.core.playback.state, PlaybackState.STOPPED) + self.assertFalse(self.playback1.stop.called) + self.assertFalse(self.playback2.stop.called) + def test_seek_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) self.core.playback.seek(10000) @@ -95,12 +137,21 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.seek.called) def test_seek_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.core.playback.seek(10000) self.assertFalse(self.playback1.seek.called) self.playback2.seek.assert_called_once_with(10000) + def test_seek_fails_for_unplayable_track(self): + self.core.playback.current_cp_track = self.unplayable_cp_track + self.core.playback.state = PlaybackState.PLAYING + success = self.core.playback.seek(1000) + + self.assertFalse(success) + self.assertFalse(self.playback1.seek.called) + self.assertFalse(self.playback2.seek.called) + def test_time_position_selects_dummy1_backend(self): self.core.playback.play(self.cp_tracks[0]) self.core.playback.seek(10000) @@ -110,9 +161,18 @@ class CorePlaybackTest(unittest.TestCase): self.assertFalse(self.playback2.get_time_position.called) def test_time_position_selects_dummy2_backend(self): - self.core.playback.play(self.cp_tracks[2]) + self.core.playback.play(self.cp_tracks[1]) self.core.playback.seek(10000) self.core.playback.time_position self.assertFalse(self.playback1.get_time_position.called) self.playback2.get_time_position.assert_called_once_with() + + def test_time_position_returns_0_if_track_is_unplayable(self): + self.core.playback.current_cp_track = self.unplayable_cp_track + + result = self.core.playback.time_position + + self.assertEqual(result, 0) + self.assertFalse(self.playback1.get_time_position.called) + self.assertFalse(self.playback2.get_time_position.called)