core: Switch to reference based stream info.

- Adds tests for new behaviors in core.
- Adds stream name to MPD format (fixes #944)
- Adds 'stream_changed' core event (needs a new name/event)
- Adds 'get_stream_reference' (which I'm also unsure about)

The bits I'm unsure about are mostly with respect to #270, but I'm going ahead
with this commit so we can discuss the details in PR with this code as an
example.
This commit is contained in:
Thomas Adamcik 2015-03-10 21:55:51 +01:00
parent e639b2b18b
commit 6fcd43891e
8 changed files with 137 additions and 58 deletions

View File

@ -5,9 +5,8 @@ import itertools
import pykka import pykka
from mopidy import audio, backend, mixer from mopidy import audio, backend, mixer, models
from mopidy.audio import PlaybackState from mopidy.audio import PlaybackState
from mopidy.audio.utils import convert_tags_to_track
from mopidy.core.history import HistoryController from mopidy.core.history import HistoryController
from mopidy.core.library import LibraryController from mopidy.core.library import LibraryController
from mopidy.core.listener import CoreListener from mopidy.core.listener import CoreListener
@ -15,7 +14,6 @@ from mopidy.core.mixer import MixerController
from mopidy.core.playback import PlaybackController from mopidy.core.playback import PlaybackController
from mopidy.core.playlists import PlaylistsController from mopidy.core.playlists import PlaylistsController
from mopidy.core.tracklist import TracklistController from mopidy.core.tracklist import TracklistController
from mopidy.models import TlTrack, Track
from mopidy.utils import versioning from mopidy.utils import versioning
from mopidy.utils.deprecation import deprecated_property from mopidy.utils.deprecation import deprecated_property
@ -88,6 +86,9 @@ class Core(
def reached_end_of_stream(self): def reached_end_of_stream(self):
self.playback.on_end_of_track() self.playback.on_end_of_track()
def stream_changed(self, uri):
self.playback.on_stream_changed(uri)
def state_changed(self, old_state, new_state, target_state): def state_changed(self, old_state, new_state, target_state):
# XXX: This is a temporary fix for issue #232 while we wait for a more # XXX: This is a temporary fix for issue #232 while we wait for a more
# permanent solution with the implementation of issue #234. When the # permanent solution with the implementation of issue #234. When the
@ -116,30 +117,15 @@ class Core(
CoreListener.send('mute_changed', mute=mute) CoreListener.send('mute_changed', mute=mute)
def tags_changed(self, tags): def tags_changed(self, tags):
if not self.audio: if not self.audio or 'title' not in tags:
return
current_tl_track = self.playback.get_current_tl_track()
if current_tl_track is None:
return return
tags = self.audio.get_current_tags().get() tags = self.audio.get_current_tags().get()
if not tags: if not tags or 'title' not in tags or not tags['title']:
return return
current_track = current_tl_track.track self.playback._stream_ref = models.Ref.track(name=tags['title'][0])
tags_track = convert_tags_to_track(tags) CoreListener.send('stream_changed')
track_kwargs = {k: v for k, v in current_track.__dict__.items() if v}
track_kwargs.update(
{k: v for k, v in tags_track.__dict__.items() if v})
self.playback._current_metadata_track = TlTrack(**{
'tlid': current_tl_track.tlid,
'track': Track(**track_kwargs)})
# TODO Move this into playback.current_metadata_track setter?
CoreListener.send('current_metadata_changed')
class Backends(list): class Backends(list):

View File

@ -164,9 +164,9 @@ class CoreListener(listener.Listener):
""" """
pass pass
def current_metadata_changed(self): def stream_changed(self):
""" """
Called whenever current track's metadata changed Called whenever the currently playing stream changes.
*MAY* be implemented by actor. *MAY* be implemented by actor.
""" """

View File

@ -20,7 +20,7 @@ class PlaybackController(object):
self.core = core self.core = core
self._current_tl_track = None self._current_tl_track = None
self._current_metadata_track = None self._stream_ref = None
self._state = PlaybackState.STOPPED self._state = PlaybackState.STOPPED
def _get_backend(self): def _get_backend(self):
@ -73,20 +73,23 @@ class PlaybackController(object):
Use :meth:`get_current_track` instead. Use :meth:`get_current_track` instead.
""" """
def get_current_metadata_track(self): def get_stream_reference(self):
""" """
Get a :class:`mopidy.models.TlTrack` with updated metadata for the Get additional information about the current stream.
currently playing track.
Returns :class:`None` if no track is currently playing. For most cases this value won't be set, but for radio streams it will
contain a reference with the name of the currently playing track or
program. Clients should show this when available.
The :class:`mopidy.models.Ref` instance may or may not have an URI set.
If present you can call ``lookup`` on it to get the full metadata for
the URI.
Returns a :class:`mopidy.models.Ref` instance representing the current
stream. If nothing is playing, or no stream info is available this will
return :class:`None`.
""" """
return self._current_metadata_track return self._stream_ref
current_metadata_track = deprecated_property(get_current_metadata_track)
"""
.. deprecated:: 0.20
Use :meth:`get_current_metadata_track` instead.
"""
def get_state(self): def get_state(self):
"""Get The playback state.""" """Get The playback state."""
@ -244,6 +247,9 @@ class PlaybackController(object):
self.stop() self.stop()
self.set_current_tl_track(None) self.set_current_tl_track(None)
def on_stream_changed(self, uri):
self._stream_ref = None
def next(self): def next(self):
""" """
Change to the next track. Change to the next track.

View File

@ -74,5 +74,5 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
def mute_changed(self, mute): def mute_changed(self, mute):
self.send_idle('output') self.send_idle('output')
def current_metadata_changed(self): def stream_changed(self):
self.send_idle('playlist') self.send_idle('playlist')

View File

@ -276,25 +276,20 @@ def plchanges(context, version):
""" """
# XXX Naive implementation that returns all tracks as changed # XXX Naive implementation that returns all tracks as changed
tracklist_version = context.core.tracklist.version.get() tracklist_version = context.core.tracklist.version.get()
iversion = int(version) if version < tracklist_version:
if iversion < tracklist_version:
return translator.tracks_to_mpd_format( return translator.tracks_to_mpd_format(
context.core.tracklist.tl_tracks.get()) context.core.tracklist.tl_tracks.get())
elif iversion == tracklist_version: elif version == tracklist_version:
# If version are equals, it is just a metadata update # A version match could indicate this is just a metadata update, so
# So we replace the updated track in playlist # check for a stream ref and let the client know about the change.
current_md_track = context.core.playback.current_metadata_track.get() stream_ref = context.core.playback.get_stream_reference().get()
if current_md_track is None: if stream_ref is None:
return None return None
ntl_tracks = [] tl_track = context.core.playback.current_tl_track.get()
tl_tracks = context.core.tracklist.tl_tracks.get() position = context.core.tracklist.index(tl_track).get()
for tl_track in tl_tracks: return translator.track_to_mpd_format(
if tl_track.tlid == current_md_track.tlid: tl_track, position=position, stream=stream_ref)
ntl_tracks.append(current_md_track)
else:
ntl_tracks.append(tl_track)
return translator.tracks_to_mpd_format(ntl_tracks)
@protocol.commands.add('plchangesposid', version=protocol.INT) @protocol.commands.add('plchangesposid', version=protocol.INT)

View File

@ -34,12 +34,12 @@ def currentsong(context):
Displays the song info of the current song (same song that is Displays the song info of the current song (same song that is
identified in status). identified in status).
""" """
tl_track = context.core.playback.current_metadata_track.get() tl_track = context.core.playback.current_tl_track.get()
if tl_track is None: stream = context.core.playback.get_stream_reference().get()
tl_track = context.core.playback.current_tl_track.get()
if tl_track is not None: if tl_track is not None:
position = context.core.tracklist.index(tl_track).get() position = context.core.tracklist.index(tl_track).get()
return translator.track_to_mpd_format(tl_track, position=position) return translator.track_to_mpd_format(
tl_track, position=position, stream=stream)
@protocol.commands.add('idle', list_command=False) @protocol.commands.add('idle', list_command=False)

View File

@ -15,7 +15,7 @@ def normalize_path(path, relative=False):
return '/'.join(parts) return '/'.join(parts)
def track_to_mpd_format(track, position=None): def track_to_mpd_format(track, position=None, stream=None):
""" """
Format track for output to MPD client. Format track for output to MPD client.
@ -33,6 +33,7 @@ def track_to_mpd_format(track, position=None):
(tlid, track) = track (tlid, track) = track
else: else:
(tlid, track) = (None, track) (tlid, track) = (None, track)
result = [ result = [
('file', track.uri or ''), ('file', track.uri or ''),
('Time', track.length and (track.length // 1000) or 0), ('Time', track.length and (track.length // 1000) or 0),
@ -41,6 +42,9 @@ def track_to_mpd_format(track, position=None):
('Album', track.album and track.album.name or ''), ('Album', track.album and track.album.name or ''),
] ]
if stream and stream.name != track.name:
result.append(('Name', stream.name))
if track.date: if track.date:
result.append(('Date', track.date)) result.append(('Date', track.date))

View File

@ -4,8 +4,12 @@ import unittest
import mock import mock
import pykka
from mopidy import backend, core from mopidy import backend, core
from mopidy.models import Track from mopidy.models import Ref, Track
from tests import dummy_audio as audio
class CorePlaybackTest(unittest.TestCase): class CorePlaybackTest(unittest.TestCase):
@ -525,3 +529,87 @@ class CorePlaybackTest(unittest.TestCase):
self.assertFalse(self.playback2.get_time_position.called) self.assertFalse(self.playback2.get_time_position.called)
# TODO Test on_tracklist_change # TODO Test on_tracklist_change
# Since we rely on our DummyAudio to actually emit events we need a "real"
# backend and not a mock so the right calls make it through to audio.
class TestBackend(pykka.ThreadingActor, backend.Backend):
uri_schemes = ['dummy']
def __init__(self, config, audio):
super(TestBackend, self).__init__()
self.playback = backend.PlaybackProvider(audio=audio, backend=self)
class TestStream(unittest.TestCase):
def setUp(self): # noqa: N802
self.audio = audio.DummyAudio.start().proxy()
self.backend = TestBackend.start(config={}, audio=self.audio).proxy()
self.core = core.Core(audio=self.audio, backends=[self.backend])
self.playback = self.core.playback
self.tracks = [Track(uri='dummy:a', length=1234),
Track(uri='dummy:b', length=1234)]
self.core.tracklist.add(self.tracks)
self.events = []
self.patcher = mock.patch('mopidy.audio.listener.AudioListener.send')
self.send_mock = self.patcher.start()
def send(event, **kwargs):
self.events.append((event, kwargs))
self.send_mock.side_effect = send
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
self.patcher.stop()
def replay_audio_events(self):
while self.events:
event, kwargs = self.events.pop(0)
self.core.on_event(event, **kwargs)
def test_get_stream_reference_before_playback(self):
self.assertEqual(self.playback.get_stream_reference(), None)
def test_get_stream_reference_during_playback(self):
self.core.playback.play()
self.replay_audio_events()
self.assertEqual(self.playback.get_stream_reference(), None)
def test_get_stream_reference_during_playback_with_tags_change(self):
self.core.playback.play()
self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get()
self.replay_audio_events()
expected = Ref.track(name='foobar')
self.assertEqual(self.playback.get_stream_reference(), expected)
def test_get_stream_reference_after_next(self):
self.core.playback.play()
self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get()
self.core.playback.next()
self.replay_audio_events()
self.assertEqual(self.playback.get_stream_reference(), None)
def test_get_stream_reference_after_next_with_tags_change(self):
self.core.playback.play()
self.audio.trigger_fake_tags_changed({'title': ['foo']}).get()
self.core.playback.next()
self.audio.trigger_fake_tags_changed({'title': ['bar']}).get()
self.replay_audio_events()
expected = Ref.track(name='bar')
self.assertEqual(self.playback.get_stream_reference(), expected)
def test_get_stream_reference_after_stop(self):
self.core.playback.play()
self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get()
self.core.playback.stop()
self.replay_audio_events()
self.assertEqual(self.playback.get_stream_reference(), None)