From b0eb2c9ac80805e24dae58780923ffe72b4a3659 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Dec 2015 21:15:33 +0100 Subject: [PATCH 01/19] playback: Tune log messages --- mopidy_spotify/playback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index 5c888147..28429d0e 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -89,7 +89,7 @@ def stop(self): return super(SpotifyPlaybackProvider, self).stop() def on_seek_data(self, time_position): - logger.debug('Audio asked us to seek to %d', time_position) + logger.debug('Audio requested seek to %d', time_position) if time_position == 0 and self._first_seek: self._first_seek = False @@ -105,7 +105,7 @@ def need_data_callback(push_audio_data_event, length_hint): # This callback is called from GStreamer/the GObject event loop. logger.log( TRACE_LOG_LEVEL, - 'Audio asked for more data (hint=%d); accepting deliveries', + 'Audio requested more data (hint=%d); accepting deliveries', length_hint) push_audio_data_event.set() @@ -113,7 +113,7 @@ def need_data_callback(push_audio_data_event, length_hint): def enough_data_callback(push_audio_data_event): # This callback is called from GStreamer/the GObject event loop. logger.log( - TRACE_LOG_LEVEL, 'Audio says it has enough data; rejecting deliveries') + TRACE_LOG_LEVEL, 'Audio has enough data; rejecting deliveries') push_audio_data_event.clear() From de8920a8a3747ec149e3439c216747d8c746469b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Dec 2015 21:17:45 +0100 Subject: [PATCH 02/19] playback: Tweak code style --- mopidy_spotify/playback.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index 28429d0e..360600ae 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -59,6 +59,7 @@ def change_track(self, track): seek_data_callback_bound = functools.partial( seek_data_callback, self.backend._actor_proxy) + self._buffer_timestamp.set(0) self._first_seek = True try: @@ -67,7 +68,6 @@ def change_track(self, track): self.backend._session.player.load(sp_track) self.backend._session.player.play() - self._buffer_timestamp.set(0) self.audio.set_appsrc( LIBSPOTIFY_GST_CAPS, need_data=need_data_callback_bound, @@ -130,7 +130,7 @@ def music_delivery_callback( # Ideally, nothing here should block. if not push_audio_data_event.is_set(): - return 0 + return 0 # Reject the audio data. It will be redelivered later. known_format = ( audio_format.sample_type == spotify.SampleType.INT16_NATIVE_ENDIAN) @@ -149,8 +149,7 @@ def music_delivery_callback( 'sample_rate': audio_format.sample_rate, } - duration = audio.calculate_duration( - num_frames, audio_format.sample_rate) + duration = audio.calculate_duration(num_frames, audio_format.sample_rate) buffer_ = audio.create_buffer( bytes(frames), capabilites=capabilites, timestamp=buffer_timestamp.get(), duration=duration) @@ -158,7 +157,9 @@ def music_delivery_callback( buffer_timestamp.increase(duration) # We must block here to know if the buffer was consumed successfully. - if audio_actor.emit_data(buffer_).get(): + consumed = audio_actor.emit_data(buffer_).get() + + if consumed: return num_frames else: return 0 From 4aecaf93b1469cc2c22c98a8cd0fa4256e138eb9 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Mon, 7 Dec 2015 21:39:40 +0000 Subject: [PATCH 03/19] lookup: ensure playlist's tracks are all loaded (Fixes #81). --- mopidy_spotify/lookup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy_spotify/lookup.py b/mopidy_spotify/lookup.py index f1f1408e..48a50d00 100644 --- a/mopidy_spotify/lookup.py +++ b/mopidy_spotify/lookup.py @@ -94,6 +94,7 @@ def _lookup_playlist(config, sp_link): sp_playlist = sp_link.as_playlist() sp_playlist.load() for sp_track in sp_playlist.tracks: + sp_track.load() track = translator.to_track( sp_track, bitrate=config['bitrate']) if track is not None: From e62c74460cd959650ddb19619f069b27d7e19309 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Mon, 7 Dec 2015 22:41:48 +0000 Subject: [PATCH 04/19] lookup: test the playlist tracks are loaded --- tests/test_lookup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_lookup.py b/tests/test_lookup.py index 80112c53..b0b332b0 100644 --- a/tests/test_lookup.py +++ b/tests/test_lookup.py @@ -144,6 +144,7 @@ def test_lookup_of_playlist_uri(session_mock, sp_playlist_mock, provider): session_mock.get_link.assert_called_once_with('spotify:playlist:alice:foo') sp_playlist_mock.link.as_playlist.assert_called_once_with() sp_playlist_mock.load.assert_called_once_with() + sp_playlist_mock.tracks[0].load.assert_called_once_with() assert len(results) == 1 track = results[0] From fb4bfcc77e9ae0d4a8f8419f7f0006ecf1217706 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Dec 2015 21:27:51 +0100 Subject: [PATCH 05/19] playback: Don't create Gst.Buffer if no audio data If we don't check this, we'll get an ValueError from Mopidy, and if Mopidy don't check it, we'll get warnings from GStreamer. --- mopidy_spotify/playback.py | 3 +++ tests/test_playback.py | 27 +++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index 360600ae..2505a670 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -132,6 +132,9 @@ def music_delivery_callback( if not push_audio_data_event.is_set(): return 0 # Reject the audio data. It will be redelivered later. + if not frames: + return 0 # No audio data; return immediately. + known_format = ( audio_format.sample_type == spotify.SampleType.INT16_NATIVE_ENDIAN) assert known_format, 'Expects 16-bit signed integer samples' diff --git a/tests/test_playback.py b/tests/test_playback.py index 81743943..a3ae61d6 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -179,8 +179,8 @@ def test_music_delivery_rejects_data_depending_on_push_audio_data_event( session_mock): audio_format = mock.Mock() - frames = b'' - num_frames = 0 + frames = b'123' + num_frames = 1 push_audio_data_event = threading.Event() buffer_timestamp = mock.Mock() assert not push_audio_data_event.is_set() @@ -192,12 +192,31 @@ def test_music_delivery_rejects_data_depending_on_push_audio_data_event( assert result == 0 +def test_music_delivery_shortcuts_if_no_data_in_frames( + session_mock, audio_lib_mock, audio_mock): + + audio_format = mock.Mock(channels=2, sample_rate=44100, sample_type=0) + frames = b'' + num_frames = 1 + push_audio_data_event = threading.Event() + push_audio_data_event.set() + buffer_timestamp = mock.Mock() + + result = playback.music_delivery_callback( + session_mock, audio_format, frames, num_frames, + audio_mock, push_audio_data_event, buffer_timestamp) + + assert result == 0 + assert audio_lib_mock.create_buffer.call_count == 0 + assert audio_mock.emit_data.call_count == 0 + + def test_music_delivery_rejects_unknown_audio_formats( session_mock, audio_mock): audio_format = mock.Mock(sample_type=17) - frames = b'' - num_frames = 0 + frames = b'123' + num_frames = 1 push_audio_data_event = threading.Event() push_audio_data_event.set() buffer_timestamp = mock.Mock() From d9add285e305c7bdbb128336fcdcc1d5547723d8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Dec 2015 00:30:17 +0100 Subject: [PATCH 06/19] playback: Add more debug logging --- mopidy_spotify/playback.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index 2505a670..30af68a9 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -51,6 +51,10 @@ def change_track(self, track): if track.uri is None: return False + logger.debug( + 'Audio requested change of track; ' + 'loading and starting Spotify player') + need_data_callback_bound = functools.partial( need_data_callback, self._push_audio_data_event) enough_data_callback_bound = functools.partial( @@ -81,10 +85,12 @@ def change_track(self, track): return False def resume(self): + logger.debug('Audio requested resume; starting Spotify player') self.backend._session.player.play() return super(SpotifyPlaybackProvider, self).resume() def stop(self): + logger.debug('Audio requested stop; pausing Spotify player') self.backend._session.player.pause() return super(SpotifyPlaybackProvider, self).stop() From a8d460eb1f2d68e3eb694a997cf0273345ba3710 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Dec 2015 00:45:54 +0100 Subject: [PATCH 07/19] playback: Ignore audio deliveries when seeking --- README.rst | 9 +++++ mopidy_spotify/playback.py | 20 +++++++++--- tests/test_playback.py | 67 ++++++++++++++++++++++++++++++++++---- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 7e3019b3..63ee0cb9 100644 --- a/README.rst +++ b/README.rst @@ -140,6 +140,15 @@ Project resources Changelog ========= +v2.3.0 (UNRELEASED) +------------------- + +Feature release. + +- Ignore all audio data deliveries from libspotify when when a seek is in + progress. This ensures that we don't deliver audio data from before the seek + with timestamps from after the seek. + v2.2.0 (2015-11-15) ------------------- diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index 30af68a9..b8035564 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -29,6 +29,7 @@ def __init__(self, *args, **kwargs): self._timeout = self.backend._config['spotify']['timeout'] self._buffer_timestamp = BufferTimestamp(0) + self._seeking_event = threading.Event() self._first_seek = False self._push_audio_data_event = threading.Event() self._push_audio_data_event.set() @@ -39,7 +40,7 @@ def _connect_events(self): self._events_connected = True self.backend._session.on( spotify.SessionEvent.MUSIC_DELIVERY, music_delivery_callback, - self.audio, self._push_audio_data_event, + self.audio, self._seeking_event, self._push_audio_data_event, self._buffer_timestamp) self.backend._session.on( spotify.SessionEvent.END_OF_TRACK, end_of_track_callback, @@ -61,7 +62,7 @@ def change_track(self, track): enough_data_callback, self._push_audio_data_event) seek_data_callback_bound = functools.partial( - seek_data_callback, self.backend._actor_proxy) + seek_data_callback, self._seeking_event, self.backend._actor_proxy) self._buffer_timestamp.set(0) self._first_seek = True @@ -98,6 +99,7 @@ def on_seek_data(self, time_position): logger.debug('Audio requested seek to %d', time_position) if time_position == 0 and self._first_seek: + self._seeking_event.clear() self._first_seek = False logger.debug('Skipping seek due to issue mopidy/mopidy#300') return @@ -123,18 +125,28 @@ def enough_data_callback(push_audio_data_event): push_audio_data_event.clear() -def seek_data_callback(spotify_backend, time_position): +def seek_data_callback(seeking_event, spotify_backend, time_position): # This callback is called from GStreamer/the GObject event loop. # It forwards the call to the backend actor. + seeking_event.set() spotify_backend.playback.on_seek_data(time_position) def music_delivery_callback( session, audio_format, frames, num_frames, - audio_actor, push_audio_data_event, buffer_timestamp): + audio_actor, seeking_event, push_audio_data_event, buffer_timestamp): # This is called from an internal libspotify thread. # Ideally, nothing here should block. + if seeking_event.is_set(): + # A seek has happened, but libspotify hasn't confirmed yet, so + # we're dropping all audio data from libspotify. + if num_frames == 0: + # libspotify signals that it has completed the seek. We'll accept + # the next audio data delivery. + seeking_event.clear() + return num_frames + if not push_audio_data_event.is_set(): return 0 # Reject the audio data. It will be redelivered later. diff --git a/tests/test_playback.py b/tests/test_playback.py index a3ae61d6..5f402488 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -61,6 +61,7 @@ def test_connect_events_adds_music_delivery_handler_to_session( spotify.SessionEvent.MUSIC_DELIVERY, playback.music_delivery_callback, audio_mock, + playback_provider._seeking_event, playback_provider._push_audio_data_event, playback_provider._buffer_timestamp) in session_mock.on.call_args_list) @@ -140,11 +141,13 @@ def test_on_seek_data_updates_timestamp_and_seeks_in_spotify( def test_on_seek_data_ignores_first_seek_to_zero_on_every_play( session_mock, provider): + provider._seeking_event.set() track = models.Track(uri='spotfy:track:test') provider.change_track(track) provider.on_seek_data(0) + assert not provider._seeking_event.is_set() assert session_mock.player.seek.call_count == 0 @@ -168,27 +171,73 @@ def test_enough_data_callback(): def test_seek_data_callback(): + seeking_event = threading.Event() backend_mock = mock.Mock() - playback.seek_data_callback(backend_mock, 1340) + playback.seek_data_callback(seeking_event, backend_mock, 1340) + assert seeking_event.is_set() backend_mock.playback.on_seek_data.assert_called_once_with(1340) +def test_music_delivery_rejects_data_when_seeking(session_mock, audio_mock): + audio_format = mock.Mock() + frames = b'123' + num_frames = 1 + seeking_event = threading.Event() + seeking_event.set() + push_audio_data_event = threading.Event() + push_audio_data_event.set() + buffer_timestamp = mock.Mock() + assert seeking_event.is_set() + + result = playback.music_delivery_callback( + session_mock, audio_format, frames, num_frames, + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) + + assert seeking_event.is_set() + assert audio_mock.emit_data.call_count == 0 + assert result == num_frames + + +def test_music_delivery_when_seeking_accepts_data_after_empty_delivery( + session_mock, audio_mock): + + audio_format = mock.Mock() + frames = b'' + num_frames = 0 + seeking_event = threading.Event() + seeking_event.set() + push_audio_data_event = threading.Event() + push_audio_data_event.set() + buffer_timestamp = mock.Mock() + assert seeking_event.is_set() + + result = playback.music_delivery_callback( + session_mock, audio_format, frames, num_frames, + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) + + assert not seeking_event.is_set() + assert audio_mock.emit_data.call_count == 0 + assert result == num_frames + + def test_music_delivery_rejects_data_depending_on_push_audio_data_event( - session_mock): + session_mock, audio_mock): audio_format = mock.Mock() frames = b'123' num_frames = 1 + seeking_event = threading.Event() push_audio_data_event = threading.Event() buffer_timestamp = mock.Mock() assert not push_audio_data_event.is_set() result = playback.music_delivery_callback( session_mock, audio_format, frames, num_frames, - audio_mock, push_audio_data_event, buffer_timestamp) + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) + assert audio_mock.emit_data.call_count == 0 assert result == 0 @@ -198,13 +247,14 @@ def test_music_delivery_shortcuts_if_no_data_in_frames( audio_format = mock.Mock(channels=2, sample_rate=44100, sample_type=0) frames = b'' num_frames = 1 + seeking_event = threading.Event() push_audio_data_event = threading.Event() push_audio_data_event.set() buffer_timestamp = mock.Mock() result = playback.music_delivery_callback( session_mock, audio_format, frames, num_frames, - audio_mock, push_audio_data_event, buffer_timestamp) + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) assert result == 0 assert audio_lib_mock.create_buffer.call_count == 0 @@ -217,6 +267,7 @@ def test_music_delivery_rejects_unknown_audio_formats( audio_format = mock.Mock(sample_type=17) frames = b'123' num_frames = 1 + seeking_event = threading.Event() push_audio_data_event = threading.Event() push_audio_data_event.set() buffer_timestamp = mock.Mock() @@ -224,7 +275,7 @@ def test_music_delivery_rejects_unknown_audio_formats( with pytest.raises(AssertionError) as excinfo: playback.music_delivery_callback( session_mock, audio_format, frames, num_frames, - audio_mock, push_audio_data_event, buffer_timestamp) + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) assert 'Expects 16-bit signed integer samples' in str(excinfo.value) @@ -238,6 +289,7 @@ def test_music_delivery_creates_gstreamer_buffer_and_gives_it_to_audio( audio_format = mock.Mock(channels=2, sample_rate=44100, sample_type=0) frames = b'\x00\x00' num_frames = 1 + seeking_event = threading.Event() push_audio_data_event = threading.Event() push_audio_data_event.set() buffer_timestamp = mock.Mock() @@ -245,7 +297,7 @@ def test_music_delivery_creates_gstreamer_buffer_and_gives_it_to_audio( result = playback.music_delivery_callback( session_mock, audio_format, frames, num_frames, - audio_mock, push_audio_data_event, buffer_timestamp) + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) audio_lib_mock.calculate_duration.assert_called_once_with(1, 44100) audio_lib_mock.create_buffer.assert_called_once_with( @@ -264,6 +316,7 @@ def test_music_delivery_consumes_zero_frames_if_audio_fails( audio_format = mock.Mock(channels=2, sample_rate=44100, sample_type=0) frames = b'\x00\x00' num_frames = 1 + seeking_event = threading.Event() push_audio_data_event = threading.Event() push_audio_data_event.set() buffer_timestamp = mock.Mock() @@ -271,7 +324,7 @@ def test_music_delivery_consumes_zero_frames_if_audio_fails( result = playback.music_delivery_callback( session_mock, audio_format, frames, num_frames, - audio_mock, push_audio_data_event, buffer_timestamp) + audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) assert result == 0 From f1635c783397596fd0525533f846285903388921 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Dec 2015 00:55:12 +0100 Subject: [PATCH 08/19] playback: Ignore duplicate end of track callbacks --- README.rst | 2 ++ mopidy_spotify/playback.py | 11 +++++++++-- tests/test_playback.py | 20 ++++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 63ee0cb9..f4716190 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,8 @@ Feature release. progress. This ensures that we don't deliver audio data from before the seek with timestamps from after the seek. +- Ignore duplicate end of track callbacks. + v2.2.0 (2015-11-15) ------------------- diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index b8035564..b7c05ae1 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -33,6 +33,7 @@ def __init__(self, *args, **kwargs): self._first_seek = False self._push_audio_data_event = threading.Event() self._push_audio_data_event.set() + self._end_of_track_event = threading.Event() self._events_connected = False def _connect_events(self): @@ -44,7 +45,7 @@ def _connect_events(self): self._buffer_timestamp) self.backend._session.on( spotify.SessionEvent.END_OF_TRACK, end_of_track_callback, - self.audio) + self._end_of_track_event, self.audio) def change_track(self, track): self._connect_events() @@ -66,6 +67,7 @@ def change_track(self, track): self._buffer_timestamp.set(0) self._first_seek = True + self._end_of_track_event.clear() try: sp_track = self.backend._session.get_track(track.uri) @@ -186,10 +188,15 @@ def music_delivery_callback( return 0 -def end_of_track_callback(session, audio_actor): +def end_of_track_callback(session, end_of_track_event, audio_actor): # This callback is called from the pyspotify event loop. + if end_of_track_event.is_set(): + logger.debug('End of track already received; ignoring callback') + return + logger.debug('End of track reached') + end_of_track_event.set() audio_actor.emit_data(None) diff --git a/tests/test_playback.py b/tests/test_playback.py index 5f402488..de704a63 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -75,7 +75,8 @@ def test_connect_events_adds_end_of_track_handler_to_session( assert (mock.call( spotify.SessionEvent.END_OF_TRACK, - playback.end_of_track_callback, audio_mock) + playback.end_of_track_callback, + playback_provider._end_of_track_event, audio_mock) in session_mock.on.call_args_list) @@ -330,11 +331,26 @@ def test_music_delivery_consumes_zero_frames_if_audio_fails( def test_end_of_track_callback(session_mock, audio_mock): - playback.end_of_track_callback(session_mock, audio_mock) + end_of_track_event = threading.Event() + playback.end_of_track_callback( + session_mock, end_of_track_event, audio_mock) + + assert end_of_track_event.is_set() audio_mock.emit_data.assert_called_once_with(None) +def test_duplicate_end_of_track_callback_is_ignored(session_mock, audio_mock): + end_of_track_event = threading.Event() + end_of_track_event.set() + + playback.end_of_track_callback( + session_mock, end_of_track_event, audio_mock) + + assert end_of_track_event.is_set() + assert audio_mock.emit_data.call_count == 0 + + def test_buffer_timestamp_wrapper(): wrapper = playback.BufferTimestamp(0) assert wrapper.get() == 0 From badbed351c4bce76919bac6bd3bd9bed5564f8f0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 14 Jan 2016 09:07:17 +0100 Subject: [PATCH 09/19] playback: Don't increase timestamp if buffer is rejected --- README.rst | 4 ++++ mopidy_spotify/playback.py | 3 +-- tests/test_playback.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f4716190..9ea7df49 100644 --- a/README.rst +++ b/README.rst @@ -151,6 +151,10 @@ Feature release. - Ignore duplicate end of track callbacks. +- Don't increase the audio buffer timestamp if the buffer is rejected by + Mopidy. This caused audio buffers delivered after one or more rejected audio + buffers to have too high timestamps. + v2.2.0 (2015-11-15) ------------------- diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index b7c05ae1..ea7feb6a 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -177,12 +177,11 @@ def music_delivery_callback( bytes(frames), capabilites=capabilites, timestamp=buffer_timestamp.get(), duration=duration) - buffer_timestamp.increase(duration) - # We must block here to know if the buffer was consumed successfully. consumed = audio_actor.emit_data(buffer_).get() if consumed: + buffer_timestamp.increase(duration) return num_frames else: return 0 diff --git a/tests/test_playback.py b/tests/test_playback.py index de704a63..57f6b09e 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -327,6 +327,7 @@ def test_music_delivery_consumes_zero_frames_if_audio_fails( session_mock, audio_format, frames, num_frames, audio_mock, seeking_event, push_audio_data_event, buffer_timestamp) + assert buffer_timestamp.increase.call_count == 0 assert result == 0 From 8116976856ffcf99ad3eb70ee0aaabcb2ff56564 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 14 Jan 2016 09:11:49 +0100 Subject: [PATCH 10/19] tox: ==dev is deprecated, test against released Mopidy instead --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index ae93a39f..c51729e4 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,7 @@ envlist = py27, flake8 [testenv] sitepackages = true deps = - mopidy==dev -rdev-requirements.txt -install_command = pip install --allow-unverified=mopidy --pre {opts} {packages} commands = py.test \ --basetemp={envtmpdir} \ From 8780c77374b44535d00a84ba1890adc4053e7cd9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 20:11:59 +0100 Subject: [PATCH 11/19] I still maintain this --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 9ea7df49..5fc326b5 100644 --- a/README.rst +++ b/README.rst @@ -137,6 +137,14 @@ Project resources - `Issue tracker `_ +Credits +======= + +- Original author: `Stein Magnus Jodal `__ +- Current maintainer: `Stein Magnus Jodal `__ +- `Contributors `_ + + Changelog ========= From fadcb5e3edeba26f0dfd3b603c89170046299db4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 20:25:58 +0100 Subject: [PATCH 12/19] docs: Fix link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5fc326b5..dbf6a568 100644 --- a/README.rst +++ b/README.rst @@ -142,7 +142,7 @@ Credits - Original author: `Stein Magnus Jodal `__ - Current maintainer: `Stein Magnus Jodal `__ -- `Contributors `_ +- `Contributors `_ Changelog From 48c633dc242dee1c9f39c64aa0794e498cae13a6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 20 Jan 2016 22:09:32 +0100 Subject: [PATCH 13/19] playback: Block in change_track() to fix gapless playback --- README.rst | 3 +++ mopidy_spotify/playback.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index dbf6a568..d6c2810a 100644 --- a/README.rst +++ b/README.rst @@ -163,6 +163,9 @@ Feature release. Mopidy. This caused audio buffers delivered after one or more rejected audio buffers to have too high timestamps. +- When changing tracks, block until Mopidy completes the appsrc URI change. + Not blocking here might break gapless playback. + v2.2.0 (2015-11-15) ------------------- diff --git a/mopidy_spotify/playback.py b/mopidy_spotify/playback.py index ea7feb6a..80f78cc5 100644 --- a/mopidy_spotify/playback.py +++ b/mopidy_spotify/playback.py @@ -75,13 +75,17 @@ def change_track(self, track): self.backend._session.player.load(sp_track) self.backend._session.player.play() - self.audio.set_appsrc( + future = self.audio.set_appsrc( LIBSPOTIFY_GST_CAPS, need_data=need_data_callback_bound, enough_data=enough_data_callback_bound, seek_data=seek_data_callback_bound) self.audio.set_metadata(track) + # Gapless playback requires that we block until URI change in + # mopidy.audio has completed before we return from change_track(). + future.get() + return True except spotify.Error as exc: logger.info('Playback of %s failed: %s', track.uri, exc) From 59b0beb10baf099d98dfb39ba0e1d306bae461ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 00:26:22 +0100 Subject: [PATCH 14/19] docs: Add PR #82 to changelog --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index d6c2810a..324ab02f 100644 --- a/README.rst +++ b/README.rst @@ -166,6 +166,9 @@ Feature release. - When changing tracks, block until Mopidy completes the appsrc URI change. Not blocking here might break gapless playback. +- Lookup of a playlist you're not subscribed to will now properly load all of + the playlist's tracks. (Fixes: #81, PR: #82) + v2.2.0 (2015-11-15) ------------------- From a77f87dc1b909ab379b4954a579c1aacff47c4e2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 00:30:48 +0100 Subject: [PATCH 15/19] travis: Run tests on trusty instead of precise Travis' APT system is quite unmaintained, so they haven't processed my request to use wheezy packages from apt.mopidy.com on precise, and are still using "stable" which is now jessie. --- .travis.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 68aedd94..2dc4f9b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,19 @@ -sudo: false +sudo: required +dist: trusty language: python python: - "2.7_with_system_site_packages" -addons: - apt: - sources: - - mopidy-stable - packages: - - libffi-dev - - libspotify-dev - - mopidy - - python-all-dev - env: - TOX_ENV=py27 - TOX_ENV=flake8 +before_install: + - "sudo apt-get update -qq" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 python-gst-1.0 libffi-dev libspotify-dev python-all-dev" + install: - "pip install tox" From a0e98ad92877af209b955643f583b4111be636ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 00:40:14 +0100 Subject: [PATCH 16/19] travis: Add apt.mopidy.com repo for libspotify-dev --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2dc4f9b8..75af7f11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ env: - TOX_ENV=flake8 before_install: + - "wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" + - "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/jessie.list" - "sudo apt-get update -qq" - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 python-gst-1.0 libffi-dev libspotify-dev python-all-dev" From 6c142dbabe7bce026dd97c3e4a13470a28dd4846 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 00:50:56 +0100 Subject: [PATCH 17/19] travis: We're not on Gst1 just yet --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 75af7f11..cbfe0d11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ before_install: - "wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" - "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/jessie.list" - "sudo apt-get update -qq" - - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 python-gst-1.0 libffi-dev libspotify-dev python-all-dev" + - "sudo apt-get install -y python-gst0.10 libffi-dev libspotify-dev python-all-dev" install: - "pip install tox" From c38ca771335648cdb45dc715a87285e3ea3e6e6d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 00:59:22 +0100 Subject: [PATCH 18/19] Workaround teardown race (see #73) --- README.rst | 3 +++ mopidy_spotify/library.py | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 324ab02f..89529535 100644 --- a/README.rst +++ b/README.rst @@ -169,6 +169,9 @@ Feature release. - Lookup of a playlist you're not subscribed to will now properly load all of the playlist's tracks. (Fixes: #81, PR: #82) +- Workaround teardown race outputing lots of short stack traces on Mopidy + shutdown. (See #73 for details) + v2.2.0 (2015-11-15) ------------------- diff --git a/mopidy_spotify/library.py b/mopidy_spotify/library.py index 8be624e1..3e95580b 100644 --- a/mopidy_spotify/library.py +++ b/mopidy_spotify/library.py @@ -4,8 +4,10 @@ from mopidy import backend -import mopidy_spotify -from mopidy_spotify import browse, distinct, images, lookup, search, utils +# Workaround https://github.com/public/flake8-import-order/issues/49: +from mopidy_spotify import Extension +from mopidy_spotify import ( + __version__, browse, distinct, images, lookup, search, utils) logger = logging.getLogger(__name__) @@ -19,9 +21,7 @@ def __init__(self, backend): self._config = backend._config['spotify'] self._requests_session = utils.get_requests_session( proxy_config=backend._config['proxy'], - user_agent='%s/%s' % ( - mopidy_spotify.Extension.dist_name, - mopidy_spotify.__version__)) + user_agent='%s/%s' % (Extension.dist_name, __version__)) def browse(self, uri): return browse.browse(self._config, self._backend._session, uri) From f7c8b96c26749126b70c601d4a5476c97a48e3e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Feb 2016 01:10:04 +0100 Subject: [PATCH 19/19] Bump version number, add release date --- README.rst | 2 +- mopidy_spotify/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 89529535..9c2d8ee6 100644 --- a/README.rst +++ b/README.rst @@ -148,7 +148,7 @@ Credits Changelog ========= -v2.3.0 (UNRELEASED) +v2.3.0 (2016-02-06) ------------------- Feature release. diff --git a/mopidy_spotify/__init__.py b/mopidy_spotify/__init__.py index de44ceec..4a5ddc52 100644 --- a/mopidy_spotify/__init__.py +++ b/mopidy_spotify/__init__.py @@ -5,7 +5,7 @@ from mopidy import config, ext -__version__ = '2.2.0' +__version__ = '2.3.0' class Extension(ext.Extension):