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
from mopidy import audio, backend, mixer
from mopidy import audio, backend, mixer, models
from mopidy.audio import PlaybackState
from mopidy.audio.utils import convert_tags_to_track
from mopidy.core.history import HistoryController
from mopidy.core.library import LibraryController
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.playlists import PlaylistsController
from mopidy.core.tracklist import TracklistController
from mopidy.models import TlTrack, Track
from mopidy.utils import versioning
from mopidy.utils.deprecation import deprecated_property
@ -88,6 +86,9 @@ class Core(
def reached_end_of_stream(self):
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):
# 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
@ -116,30 +117,15 @@ class Core(
CoreListener.send('mute_changed', mute=mute)
def tags_changed(self, tags):
if not self.audio:
return
current_tl_track = self.playback.get_current_tl_track()
if current_tl_track is None:
if not self.audio or 'title' not in tags:
return
tags = self.audio.get_current_tags().get()
if not tags:
if not tags or 'title' not in tags or not tags['title']:
return
current_track = current_tl_track.track
tags_track = convert_tags_to_track(tags)
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')
self.playback._stream_ref = models.Ref.track(name=tags['title'][0])
CoreListener.send('stream_changed')
class Backends(list):

View File

@ -164,9 +164,9 @@ class CoreListener(listener.Listener):
"""
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.
"""

View File

@ -20,7 +20,7 @@ class PlaybackController(object):
self.core = core
self._current_tl_track = None
self._current_metadata_track = None
self._stream_ref = None
self._state = PlaybackState.STOPPED
def _get_backend(self):
@ -73,20 +73,23 @@ class PlaybackController(object):
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
currently playing track.
Get additional information about the current stream.
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
current_metadata_track = deprecated_property(get_current_metadata_track)
"""
.. deprecated:: 0.20
Use :meth:`get_current_metadata_track` instead.
"""
return self._stream_ref
def get_state(self):
"""Get The playback state."""
@ -244,6 +247,9 @@ class PlaybackController(object):
self.stop()
self.set_current_tl_track(None)
def on_stream_changed(self, uri):
self._stream_ref = None
def next(self):
"""
Change to the next track.

View File

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

View File

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

View File

@ -15,7 +15,7 @@ def normalize_path(path, relative=False):
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.
@ -33,6 +33,7 @@ def track_to_mpd_format(track, position=None):
(tlid, track) = track
else:
(tlid, track) = (None, track)
result = [
('file', track.uri or ''),
('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 ''),
]
if stream and stream.name != track.name:
result.append(('Name', stream.name))
if track.date:
result.append(('Date', track.date))

View File

@ -4,8 +4,12 @@ import unittest
import mock
import pykka
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):
@ -525,3 +529,87 @@ class CorePlaybackTest(unittest.TestCase):
self.assertFalse(self.playback2.get_time_position.called)
# 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)