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:
parent
e639b2b18b
commit
6fcd43891e
@ -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):
|
||||||
|
|||||||
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user