From 983148a9a42cb110de80574801aff532ce917e7d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 18 Dec 2014 23:25:24 +0100 Subject: [PATCH] audio: Start storing the tags we find in audio Adds a new get_currents_tags method for fetching the full set of current tags. There are still some untested cases for this, and I also suspect we still want some API refinements one core starts using this. --- mopidy/audio/actor.py | 29 +++++++++++++++++--- mopidy/audio/dummy.py | 9 ++++++- tests/audio/test_actor.py | 56 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9ec85f4c..63c6a80b 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -372,6 +372,7 @@ class _Handler(object): def on_end_of_stream(self): gst_logger.debug('Got end-of-stream message.') logger.debug('Audio event: reached_end_of_stream()') + self._audio._tags = {} AudioListener.send('reached_end_of_stream') def on_error(self, error, debug): @@ -390,10 +391,10 @@ class _Handler(object): 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) + tags = utils.convert_taglist(taglist) + self._audio._tags.update(tags) + logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) + AudioListener.send('tags_changed', tags=tags.keys()) def on_missing_plugin(self, msg): desc = gst.pbutils.missing_plugin_message_get_description(msg) @@ -440,6 +441,7 @@ class Audio(pykka.ThreadingActor): self._config = config self._target_state = gst.STATE_NULL self._buffering = False + self._tags = {} self._playbin = None self._outputs = None @@ -546,6 +548,7 @@ class Audio(pykka.ThreadingActor): :param uri: the URI to play :type uri: string """ + self._tags = {} # TODO: add test for this somehow self._playbin.set_property('uri', uri) def set_appsrc( @@ -733,6 +736,7 @@ class Audio(pykka.ThreadingActor): # of faking it in the message handling when result=OK return True + # TODO: bake this into setup appsrc perhaps? def set_metadata(self, track): """ Set track metadata for currently playing song. @@ -763,5 +767,22 @@ class Audio(pykka.ThreadingActor): taglist[gst.TAG_ALBUM] = track.album.name event = gst.event_new_tag(taglist) + # TODO: check if we get this back on our own bus? self._playbin.send_event(event) gst_logger.debug('Sent tag event: track=%s', track.uri) + + def get_current_tags(self): + """ + Get the currently playing media's tags. + + If no tags have been found, or nothing is playing this returns an empty + dictionary. For each set of tags we collect a tags_changed event is + emitted with the keys of the changes tags. After such calls users may + call this function to get the updated values. + + :rtype: {key: [values]} dict for the current media. + """ + # TODO: should this be a (deep) copy? most likely yes + # TODO: should we return None when stopped? + # TODO: support only fetching keys we care about? + return self._tags diff --git a/mopidy/audio/dummy.py b/mopidy/audio/dummy.py index e67ebed2..95b9d0fb 100644 --- a/mopidy/audio/dummy.py +++ b/mopidy/audio/dummy.py @@ -21,9 +21,11 @@ class DummyAudio(pykka.ThreadingActor): self._callback = None self._uri = None self._state_change_result = True + self._tags = {} def set_uri(self, uri): assert self._uri is None, 'prepare change not called before set' + self._tags = {} self._uri = uri def set_appsrc(self, *args, **kwargs): @@ -66,6 +68,9 @@ class DummyAudio(pykka.ThreadingActor): def set_metadata(self, track): pass + def get_current_tags(self): + return self._tags + def set_about_to_finish_callback(self, callback): self._callback = callback @@ -92,7 +97,8 @@ class DummyAudio(pykka.ThreadingActor): new_state=new_state, target_state=None) if new_state == PlaybackState.PLAYING: - AudioListener.send('tags_changed', tags=[]) + self._tags['audio-codec'] = [u'fake info...'] + AudioListener.send('tags_changed', tags=['audio-codec']) return self._state_change_result @@ -107,6 +113,7 @@ class DummyAudio(pykka.ThreadingActor): self._callback() if not self._uri or not self._callback: + self._tags = {} AudioListener.send('reached_end_of_stream') else: AudioListener.send('position_changed', position=0) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 4ae9de63..f77505b7 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -338,7 +338,7 @@ class AudioEventTest(BaseTest): if not event.wait(timeout=1.0): self.fail('End of stream not reached within deadline') - # Make sure that gapless really works: + self.assertFalse(self.audio.get_current_tags().get()) def test_gapless(self, send_mock): uris = self.uris[1:] @@ -380,6 +380,60 @@ class AudioEventTest(BaseTest): self.assertEqual(1, keys.count('state_changed')) self.assertEqual(1, keys.count('reached_end_of_stream')) + # TODO: test tag states within gaples + + def test_current_tags_are_blank_to_begin_with(self, send_mock): + self.assertFalse(self.audio.get_current_tags().get()) + + def test_current_tags_blank_after_end_of_stream(self, send_mock): + done = threading.Event() + + def send(name, **kwargs): + if name == 'reached_end_of_stream': + done.set() + + send_mock.side_effect = send + + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.audio.start_playback() + + self.possibly_trigger_fake_about_to_finish() + self.audio.wait_for_state_change().get() + + if not done.wait(timeout=1.0): + self.fail('EOS not received') + + self.assertFalse(self.audio.get_current_tags().get()) + + def test_current_tags_stored(self, send_mock): + done = threading.Event() + tags = [] + + def callback(): + tags.append(self.audio.get_current_tags().get()) + + def send(name, **kwargs): + if name == 'reached_end_of_stream': + done.set() + + send_mock.side_effect = send + self.audio.set_about_to_finish_callback(callback).get() + + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.audio.start_playback() + + self.possibly_trigger_fake_about_to_finish() + self.audio.wait_for_state_change().get() + + if not done.wait(timeout=1.0): + self.fail('EOS not received') + + self.assertTrue(tags[0]) + + # TODO: test that we reset when we expect between songs + class AudioDummyEventTest(DummyMixin, AudioEventTest): """Exercise the AudioEventTest against our mock audio classes."""