diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index ae5a4383..197ba90e 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -65,6 +65,16 @@ class BasePlaybackProvider(object): """ return self.backend.audio.stop_playback().get() + def get_time_position(self): + """ + Get the current time position in milliseconds. + + *MAY be reimplemented by subclass.* + + :rtype: int + """ + return self.backend.audio.get_position().get() + def get_volume(self): """ Get current volume diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 3ada0052..9c4a0e69 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -56,6 +56,7 @@ class DummyLibraryProvider(base.BaseLibraryProvider): class DummyPlaybackProvider(base.BasePlaybackProvider): def __init__(self, *args, **kwargs): super(DummyPlaybackProvider, self).__init__(*args, **kwargs) + self._time_position = 0 self._volume = None def pause(self): @@ -63,17 +64,22 @@ class DummyPlaybackProvider(base.BasePlaybackProvider): def play(self, track): """Pass None as track to force failure""" + self._time_position = 0 return track is not None def resume(self): return True def seek(self, time_position): + self._time_position = time_position return True def stop(self): return True + def get_time_position(self): + return self._time_position + def get_volume(self): return self._volume diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index db86e56f..c321c6e9 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -41,7 +41,7 @@ class LocalBackend(ThreadingActor, base.Backend): provider=library_provider) playback_provider = base.BasePlaybackProvider(backend=self) - self.playback = LocalPlaybackController(backend=self, + self.playback = core.PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) @@ -59,18 +59,6 @@ class LocalBackend(ThreadingActor, base.Backend): self.audio = audio_refs[0].proxy() -class LocalPlaybackController(core.PlaybackController): - def __init__(self, *args, **kwargs): - super(LocalPlaybackController, self).__init__(*args, **kwargs) - - # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'? - self.stop() - - @property - def time_position(self): - return self.backend.audio.get_position().get() - - class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 1c20da87..61696bd8 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -1,18 +1,35 @@ import logging +import time from spotify import Link, SpotifyError from mopidy.backends.base import BasePlaybackProvider from mopidy.core import PlaybackState + logger = logging.getLogger('mopidy.backends.spotify.playback') + class SpotifyPlaybackProvider(BasePlaybackProvider): + def __init__(self, *args, **kwargs): + super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs) + + self._play_time_accumulated = 0 + self._play_time_started = 0 + + def pause(self): + time_since_started = self._wall_time() - self._play_time_started + self._play_time_accumulated += time_since_started + + return super(SpotifyPlaybackProvider, self).pause() + def play(self, track): - if self.backend.playback.state == PlaybackState.PLAYING: - self.backend.spotify.session.play(0) if track.uri is None: return False + + self._play_time_accumulated = 0 + self._play_time_started = self._wall_time() + try: self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) @@ -27,14 +44,37 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return False def resume(self): + self._play_time_started = self._wall_time() return self.seek(self.backend.playback.time_position) def seek(self, time_position): + self._play_time_started = self._wall_time() + self._play_time_accumulated = time_position + self.backend.audio.prepare_change() self.backend.spotify.session.seek(time_position) self.backend.audio.start_playback() + return True def stop(self): self.backend.spotify.session.play(0) return super(SpotifyPlaybackProvider, self).stop() + + def get_time_position(self): + # XXX: The default implementation of get_time_position hangs/times out + # when used with the Spotify backend and GStreamer appsrc. If this can + # be resolved, we no longer need to use a wall clock based time + # position for Spotify playback. + state = self.backend.playback.state + if state == PlaybackState.PLAYING: + time_since_started = (self._wall_time() - + self._play_time_started) + return self._play_time_accumulated + time_since_started + elif state == PlaybackState.PAUSED: + return self._play_time_accumulated + elif state == PlaybackState.STOPPED: + return 0 + + def _wall_time(self): + return int(time.time() * 1000) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 9f6030c1..82a11064 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -1,6 +1,5 @@ import logging import random -import time from mopidy.listeners import BackendListener @@ -20,7 +19,6 @@ def option_wrapper(name, default): return property(get_option, set_option) - class PlaybackState(object): """ Enum of playback states. @@ -87,8 +85,6 @@ class PlaybackController(object): self._state = PlaybackState.STOPPED self._shuffled = [] self._first_shuffle = True - self.play_time_accumulated = 0 - self.play_time_started = 0 def _get_cpid(self, cp_track): if cp_track is None: @@ -292,44 +288,10 @@ class PlaybackController(object): self._trigger_playback_state_changed(old_state, new_state) - # FIXME play_time stuff assumes backend does not have a better way of - # handeling this stuff :/ - if (old_state in (PlaybackState.PLAYING, PlaybackState.STOPPED) - and new_state == PlaybackState.PLAYING): - self._play_time_start() - elif (old_state == PlaybackState.PLAYING - and new_state == PlaybackState.PAUSED): - self._play_time_pause() - elif (old_state == PlaybackState.PAUSED - and new_state == PlaybackState.PLAYING): - self._play_time_resume() - @property def time_position(self): """Time position in milliseconds.""" - if self.state == PlaybackState.PLAYING: - time_since_started = (self._current_wall_time - - self.play_time_started) - return self.play_time_accumulated + time_since_started - elif self.state == PlaybackState.PAUSED: - return self.play_time_accumulated - elif self.state == PlaybackState.STOPPED: - return 0 - - def _play_time_start(self): - self.play_time_accumulated = 0 - self.play_time_started = self._current_wall_time - - def _play_time_pause(self): - time_since_started = self._current_wall_time - self.play_time_started - self.play_time_accumulated += time_since_started - - def _play_time_resume(self): - self.play_time_started = self._current_wall_time - - @property - def _current_wall_time(self): - return int(time.time() * 1000) + return self.provider.get_time_position() @property def volume(self): @@ -490,9 +452,6 @@ class PlaybackController(object): self.next() return True - self.play_time_started = self._current_wall_time - self.play_time_accumulated = time_position - success = self.provider.seek(time_position) if success: self._trigger_seeked(time_position) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 59418a3b..2397b96f 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -159,18 +159,21 @@ class StatusHandlerTest(unittest.TestCase): self.assertLessEqual(position, total) def test_status_method_when_playing_contains_elapsed(self): - self.backend.playback.state = PAUSED - self.backend.playback.play_time_accumulated = 59123 + self.backend.current_playlist.append([Track(length=60000)]) + self.backend.playback.play() + self.backend.playback.pause() + self.backend.playback.seek(59123) result = dict(status.status(self.context)) self.assertIn('elapsed', result) self.assertEqual(result['elapsed'], '59.123') def test_status_method_when_starting_playing_contains_elapsed_zero(self): - self.backend.playback.state = PAUSED - self.backend.playback.play_time_accumulated = 123 # Less than 1000ms + self.backend.current_playlist.append([Track(length=10000)]) + self.backend.playback.play() + self.backend.playback.pause() result = dict(status.status(self.context)) self.assertIn('elapsed', result) - self.assertEqual(result['elapsed'], '0.123') + self.assertEqual(result['elapsed'], '0.000') def test_status_method_when_playing_contains_bitrate(self): self.backend.current_playlist.append([Track(bitrate=320)])