diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 190895dc..3bc62a29 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -303,6 +303,8 @@ class _Handler(object): self.on_warning(*msg.parse_warning()) elif msg.type == gst.MESSAGE_ASYNC_DONE: self.on_async_done() + elif msg.type == gst.MESSAGE_TAG: + self.on_tag(msg.parse_tag()) elif msg.type == gst.MESSAGE_ELEMENT: if gst.pbutils.is_missing_plugin_message(msg): self.on_missing_plugin(_get_missing_description(msg), @@ -387,6 +389,12 @@ class _Handler(object): def on_async_done(self): gst_logger.debug('Got async-done.') + def on_tag(self, taglist): + # TODO: store current tags and reset on stream changes. + tags = taglist.keys() + logger.debug('Audio event: tags_changed(tags=%r)', tags) + AudioListener.send('tags_changed', tags=tags) + def on_missing_plugin(self, msg): desc = gst.pbutils.missing_plugin_message_get_description(msg) debug = gst.pbutils.missing_plugin_message_get_installer_detail(msg) diff --git a/mopidy/audio/dummy.py b/mopidy/audio/dummy.py index f7fa9f0d..e67ebed2 100644 --- a/mopidy/audio/dummy.py +++ b/mopidy/audio/dummy.py @@ -91,6 +91,9 @@ class DummyAudio(pykka.ThreadingActor): AudioListener.send('state_changed', old_state=old_state, new_state=new_state, target_state=None) + if new_state == PlaybackState.PLAYING: + AudioListener.send('tags_changed', tags=[]) + return self._state_change_result def trigger_fake_playback_failure(self): diff --git a/mopidy/audio/listener.py b/mopidy/audio/listener.py index 6beb4444..9961cf54 100644 --- a/mopidy/audio/listener.py +++ b/mopidy/audio/listener.py @@ -75,3 +75,21 @@ class AudioListener(listener.Listener): field or :class:`None` if this is a final state. """ pass + + def tags_changed(self, tags): + """ + Called whenever the current audio streams tags changes. + + This event signals that some track metadata has been updated. This can + be metadata such as artists, titles, organization, or details about the + actual audio such as bit-rates, numbers of channels etc. + + For the available tag keys please refer to GStreamer documenation for + tags. + + *MAY* be implemented by actor. + + :param tags: The tags that have just been updated. + :type tags: :class:`set` of strings + """ + pass diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index ab897595..4ae9de63 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -42,7 +42,7 @@ class BaseTest(unittest.TestCase): audio_class = audio.Audio - def setUp(self): + def setUp(self): # noqa config = { 'audio': { 'mixer': 'foomixer', @@ -57,7 +57,7 @@ class BaseTest(unittest.TestCase): self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) self.audio = self.audio_class.start(config=config, mixer=None).proxy() - def tearDown(self): + def tearDown(self): # noqa pykka.ActorRegistry.stop_all() def possibly_trigger_fake_playback_error(self): @@ -135,7 +135,7 @@ class AudioDummyTest(DummyMixin, AudioTest): @mock.patch.object(audio.AudioListener, 'send') class AudioEventTest(BaseTest): - def setUp(self): + def setUp(self): # noqa super(AudioEventTest, self).setUp() self.audio.enable_sync_handler().get() @@ -292,6 +292,14 @@ class AudioEventTest(BaseTest): call = mock.call('position_changed', position=2000) self.assertIn(call, send_mock.call_args_list) + def test_tags_changed_on_playback(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.audio.start_playback() + self.audio.wait_for_state_change().get() + + send_mock.assert_any_call('tags_changed', tags=mock.ANY) + # Unlike the other events, having the state changed done is not # enough to ensure our event is called. So we setup a threading # event that we can wait for with a timeout while the track playback @@ -361,20 +369,20 @@ class AudioEventTest(BaseTest): if not done.wait(timeout=1.0): self.fail('EOS not received') - excepted = [ - ('position_changed', {'position': 0}), - ('stream_changed', {'uri': self.uris[0]}), - ('state_changed', {'old_state': PlaybackState.STOPPED, - 'new_state': PlaybackState.PLAYING, - 'target_state': None}), - ('position_changed', {'position': 0}), - ('stream_changed', {'uri': self.uris[1]}), - ('reached_end_of_stream', {})] - self.assertEqual(excepted, events) + # Check that both uris got played + self.assertIn(('stream_changed', {'uri': self.uris[0]}), events) + self.assertIn(('stream_changed', {'uri': self.uris[1]}), events) + + # Check that events counts check out. + keys = [k for k, v in events] + self.assertEqual(2, keys.count('stream_changed')) + self.assertEqual(2, keys.count('position_changed')) + self.assertEqual(1, keys.count('state_changed')) + self.assertEqual(1, keys.count('reached_end_of_stream')) class AudioDummyEventTest(DummyMixin, AudioEventTest): - pass + """Exercise the AudioEventTest against our mock audio classes.""" # TODO: move to mixer tests... @@ -399,7 +407,7 @@ class MixerTest(BaseTest): class AudioStateTest(unittest.TestCase): - def setUp(self): + def setUp(self): # noqa self.audio = audio.Audio(config=None, mixer=None) def test_state_starts_as_stopped(self): @@ -444,7 +452,7 @@ class AudioStateTest(unittest.TestCase): class AudioBufferingTest(unittest.TestCase): - def setUp(self): + def setUp(self): # noqa self.audio = audio.Audio(config=None, mixer=None) self.audio._playbin = mock.Mock(spec=['set_state']) diff --git a/tests/audio/test_listener.py b/tests/audio/test_listener.py index 08b03e6c..6b78ecb0 100644 --- a/tests/audio/test_listener.py +++ b/tests/audio/test_listener.py @@ -8,7 +8,7 @@ from mopidy import audio class AudioListenerTest(unittest.TestCase): - def setUp(self): + def setUp(self): # noqa self.listener = audio.AudioListener() def test_on_event_forwards_to_specific_handler(self): @@ -32,3 +32,6 @@ class AudioListenerTest(unittest.TestCase): def test_listener_has_default_impl_for_position_changed(self): self.listener.position_changed(None) + + def test_listener_has_default_impl_for_tags_changed(self): + self.listener.tags_changed([])