Merge pull request #1288 from adamcik/feature/implement-gapless

Gapless part 1
This commit is contained in:
Stein Magnus Jodal 2015-10-07 23:32:31 +02:00
commit 93899c8599
16 changed files with 1141 additions and 928 deletions

View File

@ -59,6 +59,18 @@ Cleanups
- Removed warning if :file:`~/.config/mopidy/settings.py` exists. We stopped
using this settings file in 0.14, released in April 2013.
Gapless
-------
- Add partial support for gapless playback. Gapless now works as long as you
don't change tracks or use next/previous. (PR: :issue:`1288`)
- Core playback has been refactored to better handle gapless, and async state
changes.
- Tests have been updated to always use a core actor so async state changes
don't trip us up.
v1.1.1 (2015-09-14)
===================

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
import logging
import os
import threading
import gobject
@ -406,6 +407,7 @@ class Audio(pykka.ThreadingActor):
self.mixer = SoftwareMixer(mixer)
def on_start(self):
self._thread = threading.current_thread()
try:
self._setup_preferences()
self._setup_playbin()
@ -499,6 +501,11 @@ class Audio(pykka.ThreadingActor):
self.mixer.teardown()
def _on_about_to_finish(self, element):
if self._thread == threading.current_thread():
logger.error(
'about-to-finish in actor, aborting to avoid deadlock.')
return
gst_logger.debug('Got about-to-finish event.')
if self._about_to_finish_callback:
logger.debug('Running about to finish callback.')

View File

@ -54,7 +54,8 @@ class Core(
self.library = LibraryController(backends=self.backends, core=self)
self.history = HistoryController()
self.mixer = MixerController(mixer=mixer)
self.playback = PlaybackController(backends=self.backends, core=self)
self.playback = PlaybackController(
audio=audio, backends=self.backends, core=self)
self.playlists = PlaylistsController(backends=self.backends, core=self)
self.tracklist = TracklistController(core=self)
@ -84,7 +85,7 @@ class Core(
"""
def reached_end_of_stream(self):
self.playback._on_end_of_track()
self.playback._on_end_of_stream()
def stream_changed(self, uri):
self.playback._on_stream_changed(uri)

View File

@ -14,21 +14,26 @@ logger = logging.getLogger(__name__)
class PlaybackController(object):
pykka_traversable = True
def __init__(self, backends, core):
def __init__(self, audio, backends, core):
# TODO: these should be internal
self.backends = backends
self.core = core
self._audio = audio
self._current_tl_track = None
self._stream_title = None
self._state = PlaybackState.STOPPED
def _get_backend(self):
# TODO: take in track instead
track = self.get_current_track()
if track is None:
self._current_tl_track = None
self._pending_tl_track = None
if self._audio:
self._audio.set_about_to_finish_callback(
self._on_about_to_finish_callback)
def _get_backend(self, tl_track):
if tl_track is None:
return None
uri = track.uri
uri_scheme = urlparse.urlparse(uri).scheme
uri_scheme = urlparse.urlparse(tl_track.track.uri).scheme
return self.backends.with_playback.get(uri_scheme, None)
# Properties
@ -122,7 +127,7 @@ class PlaybackController(object):
def get_time_position(self):
"""Get time position in milliseconds."""
backend = self._get_backend()
backend = self._get_backend(self.get_current_tl_track())
if backend:
return backend.playback.get_time_position().get()
else:
@ -190,49 +195,45 @@ class PlaybackController(object):
# Methods
# TODO: remove this.
def _change_track(self, tl_track, on_error_step=1):
"""
Change to the given track, keeping the current playback state.
def _on_end_of_stream(self):
self.set_state(PlaybackState.STOPPED)
self._set_current_tl_track(None)
# TODO: self._trigger_track_playback_ended?
:param tl_track: track to change to
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track. **INTERNAL**
:type on_error_step: int, -1 or 1
"""
old_state = self.get_state()
self.stop()
self._set_current_tl_track(tl_track)
if old_state == PlaybackState.PLAYING:
self._play(on_error_step=on_error_step)
elif old_state == PlaybackState.PAUSED:
# NOTE: this is just a quick hack to fix #1177 as this code has
# already been killed in the gapless branch.
backend = self._get_backend()
if backend:
backend.playback.prepare_change()
backend.playback.change_track(tl_track.track).get()
self.pause()
def _on_stream_changed(self, uri):
self._stream_title = None
if self._pending_tl_track:
self._set_current_tl_track(self._pending_tl_track)
self._pending_tl_track = None
self._trigger_track_playback_started()
# TODO: this is not really end of track, this is on_need_next_track
def _on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
def _on_about_to_finish_callback(self):
"""Callback that performs a blocking actor call to the real callback.
Used by event handler in :class:`mopidy.core.Core`.
This is passed to audio, which is allowed to call this code from the
audio thread. We pass execution into the core actor to ensure that
there is no unsafe access of state in core. This must block until
we get a response.
"""
if self.get_state() == PlaybackState.STOPPED:
return
self.core.actor_ref.ask({
'command': 'pykka_call', 'args': tuple(), 'kwargs': {},
'attr_path': ('playback', '_on_about_to_finish'),
})
def _on_about_to_finish(self):
self._trigger_track_playback_ended(self.get_time_position())
# TODO: check that we always have a current track
original_tl_track = self.get_current_tl_track()
next_tl_track = self.core.tracklist.eot_track(original_tl_track)
if next_tl_track:
self._change_track(next_tl_track)
else:
self.stop()
self._set_current_tl_track(None)
# TODO: only set pending if we have a backend that can play it?
# TODO: skip tracks that don't have a backend?
self._pending_tl_track = next_tl_track
backend = self._get_backend(next_tl_track)
if backend:
backend.playback.change_track(next_tl_track.track).get()
self.core.tracklist._mark_played(original_tl_track)
@ -242,13 +243,11 @@ class PlaybackController(object):
Used by :class:`mopidy.core.TracklistController`.
"""
tracklist = self.core.tracklist.get_tl_tracks()
if self.get_current_tl_track() not in tracklist:
if not self.core.tracklist.tl_tracks:
self.stop()
self._set_current_tl_track(None)
def _on_stream_changed(self, uri):
self._stream_title = None
elif self.get_current_tl_track() not in self.core.tracklist.tl_tracks:
self._set_current_tl_track(None)
def next(self):
"""
@ -257,23 +256,29 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
original_tl_track = self.get_current_tl_track()
next_tl_track = self.core.tracklist.next_track(original_tl_track)
state = self.get_state()
current = self._pending_tl_track or self._current_tl_track
if next_tl_track:
# TODO: switch to:
# backend.play(track)
# wait for state change?
self._change_track(next_tl_track)
else:
self.stop()
self._set_current_tl_track(None)
# TODO: move to pending track?
self._trigger_track_playback_ended(self.get_time_position())
self.core.tracklist._mark_played(self._current_tl_track)
self.core.tracklist._mark_played(original_tl_track)
while current:
pending = self.core.tracklist.next_track(current)
if self._change(pending, state):
break
else:
self.core.tracklist._mark_unplayable(pending)
# TODO: this could be needed to prevent a loop in rare cases
# if current == pending:
# break
current = pending
# TODO return result?
def pause(self):
"""Pause playback."""
backend = self._get_backend()
backend = self._get_backend(self.get_current_tl_track())
if not backend or backend.playback.pause().get():
# TODO: switch to:
# backend.track(pause)
@ -302,9 +307,6 @@ class PlaybackController(object):
if tl_track:
deprecation.warn('core.playback.play:tl_track_kwarg', pending=True)
self._play(tl_track=tl_track, tlid=tlid, on_error_step=1)
def _play(self, tl_track=None, tlid=None, on_error_step=1):
if tl_track is None and tlid is not None:
for tl_track in self.core.tracklist.get_tl_tracks():
if tl_track.tlid == tlid:
@ -312,60 +314,71 @@ class PlaybackController(object):
else:
tl_track = None
if tl_track is None:
if self.get_state() == PlaybackState.PAUSED:
return self.resume()
if tl_track is not None:
# TODO: allow from outside tracklist, would make sense given refs?
assert tl_track in self.core.tracklist.get_tl_tracks()
elif tl_track is None and self.get_state() == PlaybackState.PAUSED:
self.resume()
return
if self.get_current_tl_track() is not None:
tl_track = self.get_current_tl_track()
original = self._current_tl_track
current = self._pending_tl_track or self._current_tl_track
pending = tl_track or current or self.core.tracklist.next_track(None)
if original != pending and self.get_state() != PlaybackState.STOPPED:
self._trigger_track_playback_ended(self.get_time_position())
if pending:
# TODO: remove?
self.set_state(PlaybackState.PLAYING)
while pending:
# TODO: should we consume unplayable tracks in this loop?
if self._change(pending, PlaybackState.PLAYING):
break
else:
if on_error_step == 1:
tl_track = self.core.tracklist.next_track(tl_track)
elif on_error_step == -1:
tl_track = self.core.tracklist.previous_track(tl_track)
self.core.tracklist._mark_unplayable(pending)
current = pending
pending = self.core.tracklist.next_track(current)
if tl_track is None:
return
# TODO: move to top and get rid of original?
self.core.tracklist._mark_played(original)
# TODO return result?
assert tl_track in self.core.tracklist.get_tl_tracks()
def _change(self, pending_tl_track, state):
self._pending_tl_track = pending_tl_track
# TODO: switch to:
# backend.play(track)
# wait for state change?
if self.get_state() == PlaybackState.PLAYING:
if not pending_tl_track:
self.stop()
self._on_end_of_stream() # pretend an EOS happened for cleanup
return True
self._set_current_tl_track(tl_track)
self.set_state(PlaybackState.PLAYING)
backend = self._get_backend()
success = False
backend = self._get_backend(pending_tl_track)
if not backend:
return False
if backend:
backend.playback.prepare_change()
backend.playback.prepare_change()
if not backend.playback.change_track(pending_tl_track.track).get():
return False # TODO: test for this path
if state == PlaybackState.PLAYING:
try:
success = (
backend.playback.change_track(tl_track.track).get() and
backend.playback.play().get())
return backend.playback.play().get()
except TypeError:
logger.error(
'%s needs to be updated to work with this '
'version of Mopidy.',
backend.actor_ref.actor_class.__name__)
logger.debug('Backend exception', exc_info=True)
# TODO: check by binding against underlying play method using
# inspect and otherwise re-raise?
logger.error('%s needs to be updated to work with this '
'version of Mopidy.', backend)
return False
elif state == PlaybackState.PAUSED:
return backend.playback.pause().get()
elif state == PlaybackState.STOPPED:
# TODO: emit some event now?
self._current_tl_track = self._pending_tl_track
self._pending_tl_track = None
return True
if success:
self.core.tracklist._mark_playing(tl_track)
self.core.history._add_track(tl_track.track)
# TODO: replace with stream-changed
self._trigger_track_playback_started()
else:
self.core.tracklist._mark_unplayable(tl_track)
if on_error_step == 1:
# TODO: can cause an endless loop for single track repeat.
self.next()
elif on_error_step == -1:
self.previous()
raise Exception('Unknown state: %s' % state)
def previous(self):
"""
@ -374,18 +387,29 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
tl_track = self.get_current_tl_track()
# TODO: switch to:
# self.play(....)
# wait for state change?
self._change_track(
self.core.tracklist.previous_track(tl_track), on_error_step=-1)
self._trigger_track_playback_ended(self.get_time_position())
state = self.get_state()
current = self._pending_tl_track or self._current_tl_track
while current:
pending = self.core.tracklist.previous_track(current)
if self._change(pending, state):
break
else:
self.core.tracklist._mark_unplayable(pending)
# TODO: this could be needed to prevent a loop in rare cases
# if current == pending:
# break
current = pending
# TODO: no return value?
def resume(self):
"""If paused, resume playing the current track."""
if self.get_state() != PlaybackState.PAUSED:
return
backend = self._get_backend()
backend = self._get_backend(self.get_current_tl_track())
if backend and backend.playback.resume().get():
self.set_state(PlaybackState.PLAYING)
# TODO: trigger via gst messages
@ -402,6 +426,7 @@ class PlaybackController(object):
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
# TODO: seek needs to take pending tracks into account :(
validation.check_integer(time_position)
if time_position < 0:
@ -412,19 +437,28 @@ class PlaybackController(object):
if not self.core.tracklist.tracks:
return False
if self.current_track and self.current_track.length is None:
return False
if self.get_state() == PlaybackState.STOPPED:
self.play()
# TODO: uncomment once we have tests for this. Should fix seek after
# about to finish doing wrong track.
# if self._current_tl_track and self._pending_tl_track:
# self.play(self._current_tl_track)
# We need to prefer the still playing track, but if nothing is playing
# we fall back to the pending one.
tl_track = self._current_tl_track or self._pending_tl_track
if tl_track and tl_track.track.length is None:
return False
if time_position < 0:
time_position = 0
elif time_position > self.current_track.length:
elif time_position > tl_track.track.length:
# TODO: gstreamer will trigger a about to finish for us, use that?
self.next()
return True
backend = self._get_backend()
backend = self._get_backend(self.get_current_tl_track())
if not backend:
return False
@ -436,7 +470,7 @@ class PlaybackController(object):
def stop(self):
"""Stop playing."""
if self.get_state() != PlaybackState.STOPPED:
backend = self._get_backend()
backend = self._get_backend(self.get_current_tl_track())
time_position_before_stop = self.get_time_position()
if not backend or backend.playback.stop().get():
self.set_state(PlaybackState.STOPPED)
@ -461,12 +495,15 @@ class PlaybackController(object):
time_position=self.get_time_position())
def _trigger_track_playback_started(self):
# TODO: replace with stream-changed
logger.debug('Triggering track playback started event')
if self.get_current_tl_track() is None:
return
listener.CoreListener.send(
'track_playback_started',
tl_track=self.get_current_tl_track())
tl_track = self.get_current_tl_track()
self.core.tracklist._mark_playing(tl_track)
self.core.history._add_track(tl_track.track)
listener.CoreListener.send('track_playback_started', tl_track=tl_track)
def _trigger_track_playback_ended(self, time_position_before_stop):
logger.debug('Triggering track playback ended event')

View File

@ -318,10 +318,11 @@ class TracklistController(object):
return self._shuffled[0]
return None
if tl_track is None:
next_index = self.index(tl_track)
if next_index is None:
next_index = 0
else:
next_index = self.index(tl_track) + 1
next_index += 1
if self.get_repeat():
next_index %= len(self._tl_tracks)

View File

@ -137,13 +137,13 @@ def pause(context, state=None):
playback_state = context.core.playback.get_state().get()
if (playback_state == PlaybackState.PLAYING):
context.core.playback.pause()
context.core.playback.pause().get()
elif (playback_state == PlaybackState.PAUSED):
context.core.playback.resume()
context.core.playback.resume().get()
elif state:
context.core.playback.pause()
context.core.playback.pause().get()
else:
context.core.playback.resume()
context.core.playback.resume().get()
@protocol.commands.add('play', songpos=protocol.INT)

View File

@ -59,7 +59,7 @@ class BaseTest(unittest.TestCase):
def tearDown(self): # noqa
pykka.ActorRegistry.stop_all()
def possibly_trigger_fake_playback_error(self):
def possibly_trigger_fake_playback_error(self, uri):
pass
def possibly_trigger_fake_about_to_finish(self):
@ -69,8 +69,8 @@ class BaseTest(unittest.TestCase):
class DummyMixin(object):
audio_class = dummy_audio.DummyAudio
def possibly_trigger_fake_playback_error(self):
self.audio.trigger_fake_playback_failure()
def possibly_trigger_fake_playback_error(self, uri):
self.audio.trigger_fake_playback_failure(uri)
def possibly_trigger_fake_about_to_finish(self):
callback = self.audio.get_about_to_finish_callback().get()
@ -86,7 +86,7 @@ class AudioTest(BaseTest):
self.assertTrue(self.audio.start_playback().get())
def test_start_playback_non_existing_file(self):
self.possibly_trigger_fake_playback_error()
self.possibly_trigger_fake_playback_error(self.uris[0] + 'bogus')
self.audio.prepare_change()
self.audio.set_uri(self.uris[0] + 'bogus')
@ -133,186 +133,253 @@ class AudioDummyTest(DummyMixin, AudioTest):
pass
@mock.patch.object(audio.AudioListener, 'send')
class DummyAudioListener(pykka.ThreadingActor, audio.AudioListener):
def __init__(self):
super(DummyAudioListener, self).__init__()
self.events = []
self.waiters = {}
def on_event(self, event, **kwargs):
self.events.append((event, kwargs))
if event in self.waiters:
self.waiters[event].set()
def wait(self, event):
self.waiters[event] = threading.Event()
return self.waiters[event]
def get_events(self):
return self.events
def clear_events(self):
self.events = []
class AudioEventTest(BaseTest):
def setUp(self): # noqa: N802
super(AudioEventTest, self).setUp()
self.audio.enable_sync_handler().get()
self.listener = DummyAudioListener.start().proxy()
def tearDown(self): # noqa: N802
super(AudioEventTest, self).setUp()
def assertEvent(self, event, **kwargs): # noqa: N802
self.assertIn((event, kwargs), self.listener.get_events().get())
def assertNotEvent(self, event, **kwargs): # noqa: N802
self.assertNotIn((event, kwargs), self.listener.get_events().get())
# TODO: test without uri set, with bad uri and gapless...
# TODO: playing->playing triggered by seek should be removed
# TODO: codify expected state after EOS
# TODO: consider returning a future or a threading event?
def test_state_change_stopped_to_playing_event(self, send_mock):
def test_state_change_stopped_to_playing_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.STOPPED,
self.assertEvent('state_changed', old_state=PlaybackState.STOPPED,
new_state=PlaybackState.PLAYING, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_state_change_stopped_to_paused_event(self, send_mock):
def test_state_change_stopped_to_paused_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.STOPPED,
self.assertEvent('state_changed', old_state=PlaybackState.STOPPED,
new_state=PlaybackState.PAUSED, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_state_change_paused_to_playing_event(self, send_mock):
def test_state_change_paused_to_playing_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.start_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.PAUSED,
self.assertEvent('state_changed', old_state=PlaybackState.PAUSED,
new_state=PlaybackState.PLAYING, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_state_change_paused_to_stopped_event(self, send_mock):
def test_state_change_paused_to_stopped_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.stop_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.PAUSED,
self.assertEvent('state_changed', old_state=PlaybackState.PAUSED,
new_state=PlaybackState.STOPPED, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_state_change_playing_to_paused_event(self, send_mock):
def test_state_change_playing_to_paused_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.pause_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.PLAYING,
self.assertEvent('state_changed', old_state=PlaybackState.PLAYING,
new_state=PlaybackState.PAUSED, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_state_change_playing_to_stopped_event(self, send_mock):
def test_state_change_playing_to_stopped_event(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.stop_playback()
self.audio.wait_for_state_change().get()
call = mock.call('state_changed', old_state=PlaybackState.PLAYING,
self.assertEvent('state_changed', old_state=PlaybackState.PLAYING,
new_state=PlaybackState.STOPPED, target_state=None)
self.assertIn(call, send_mock.call_args_list)
def test_stream_changed_event_on_playing(self, send_mock):
def test_stream_changed_event_on_playing(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.listener.clear_events()
self.audio.start_playback()
# Since we are going from stopped to playing, the state change is
# enough to ensure the stream changed.
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=self.uris[0])
call = mock.call('stream_changed', uri=self.uris[0])
self.assertIn(call, send_mock.call_args_list)
def test_stream_changed_event_on_multiple_changes(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.listener.clear_events()
self.audio.start_playback()
def test_stream_changed_event_on_paused_to_stopped(self, send_mock):
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=self.uris[0])
self.audio.prepare_change()
self.audio.set_uri(self.uris[1])
self.audio.pause_playback()
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=self.uris[1])
def test_stream_changed_event_on_playing_to_paused(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.listener.clear_events()
self.audio.start_playback()
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=self.uris[0])
self.listener.clear_events()
self.audio.pause_playback()
self.audio.wait_for_state_change().get()
self.assertNotEvent('stream_changed', uri=self.uris[0])
def test_stream_changed_event_on_paused_to_stopped(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.stop_playback()
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=None)
call = mock.call('stream_changed', uri=None)
self.assertIn(call, send_mock.call_args_list)
def test_position_changed_on_pause(self, send_mock):
def test_position_changed_on_pause(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change()
self.audio.wait_for_state_change().get()
self.assertEvent('position_changed', position=0)
call = mock.call('position_changed', position=0)
self.assertIn(call, send_mock.call_args_list)
def test_stream_changed_event_on_paused_to_playing(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.listener.clear_events()
self.audio.pause_playback()
def test_position_changed_on_play(self, send_mock):
self.audio.wait_for_state_change().get()
self.assertEvent('stream_changed', uri=self.uris[0])
self.listener.clear_events()
self.audio.start_playback()
self.audio.wait_for_state_change().get()
self.assertNotEvent('stream_changed', uri=self.uris[0])
def test_position_changed_on_play(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()
self.audio.wait_for_state_change()
self.audio.wait_for_state_change().get()
self.assertEvent('position_changed', position=0)
call = mock.call('position_changed', position=0)
self.assertIn(call, send_mock.call_args_list)
def test_position_changed_on_seek(self, send_mock):
def test_position_changed_on_seek_while_stopped(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.set_position(2000)
self.audio.wait_for_state_change().get()
self.assertNotEvent('position_changed', position=0)
call = mock.call('position_changed', position=0)
self.assertNotIn(call, send_mock.call_args_list)
def test_position_changed_on_seek_after_play(self, send_mock):
def test_position_changed_on_seek_after_play(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.set_position(2000)
self.audio.wait_for_state_change().get()
self.assertEvent('position_changed', position=2000)
call = mock.call('position_changed', position=2000)
self.assertIn(call, send_mock.call_args_list)
def test_position_changed_on_seek_after_pause(self, send_mock):
def test_position_changed_on_seek_after_pause(self):
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
self.audio.pause_playback()
self.audio.wait_for_state_change()
self.listener.clear_events()
self.audio.set_position(2000)
self.audio.wait_for_state_change().get()
self.assertEvent('position_changed', position=2000)
call = mock.call('position_changed', position=2000)
self.assertIn(call, send_mock.call_args_list)
def test_tags_changed_on_playback(self, send_mock):
def test_tags_changed_on_playback(self):
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)
self.assertEvent('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
# completes.
def test_stream_changed_event_on_paused(self, send_mock):
event = threading.Event()
def send(name, **kwargs):
if name == 'stream_changed':
event.set()
send_mock.side_effect = send
def test_stream_changed_event_on_paused(self):
event = self.listener.wait('stream_changed').get()
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
@ -322,13 +389,10 @@ class AudioEventTest(BaseTest):
if not event.wait(timeout=1.0):
self.fail('Stream changed not reached within deadline')
def test_reached_end_of_stream_event(self, send_mock):
event = threading.Event()
self.assertEvent('stream_changed', uri=self.uris[0])
def send(name, **kwargs):
if name == 'reached_end_of_stream':
event.set()
send_mock.side_effect = send
def test_reached_end_of_stream_event(self):
event = self.listener.wait('reached_end_of_stream').get()
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
@ -341,21 +405,14 @@ class AudioEventTest(BaseTest):
self.assertFalse(self.audio.get_current_tags().get())
def test_gapless(self, send_mock):
def test_gapless(self):
uris = self.uris[1:]
events = []
done = threading.Event()
event = self.listener.wait('reached_end_of_stream').get()
def callback():
if uris:
self.audio.set_uri(uris.pop()).get()
def send(name, **kwargs):
events.append((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()
@ -367,15 +424,15 @@ class AudioEventTest(BaseTest):
self.possibly_trigger_fake_about_to_finish()
self.audio.wait_for_state_change().get()
if not done.wait(timeout=1.0):
if not event.wait(timeout=1.0):
self.fail('EOS not received')
# Check that both uris got played
self.assertIn(('stream_changed', {'uri': self.uris[0]}), events)
self.assertIn(('stream_changed', {'uri': self.uris[1]}), events)
self.assertEvent('stream_changed', uri=self.uris[0])
self.assertEvent('stream_changed', uri=self.uris[1])
# Check that events counts check out.
keys = [k for k, v in events]
keys = [k for k, v in self.listener.get_events().get()]
self.assertEqual(2, keys.count('stream_changed'))
self.assertEqual(2, keys.count('position_changed'))
self.assertEqual(1, keys.count('state_changed'))
@ -383,17 +440,12 @@ class AudioEventTest(BaseTest):
# TODO: test tag states within gaples
def test_current_tags_are_blank_to_begin_with(self, send_mock):
# TODO: this does not belong in this testcase
def test_current_tags_are_blank_to_begin_with(self):
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
def test_current_tags_blank_after_end_of_stream(self):
event = self.listener.wait('reached_end_of_stream').get()
self.audio.prepare_change()
self.audio.set_uri(self.uris[0])
@ -402,23 +454,18 @@ class AudioEventTest(BaseTest):
self.possibly_trigger_fake_about_to_finish()
self.audio.wait_for_state_change().get()
if not done.wait(timeout=1.0):
if not event.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()
def test_current_tags_stored(self):
event = self.listener.wait('reached_end_of_stream').get()
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()
@ -428,7 +475,7 @@ class AudioEventTest(BaseTest):
self.possibly_trigger_fake_about_to_finish()
self.audio.wait_for_state_change().get()
if not done.wait(timeout=1.0):
if not event.wait(timeout=1.0):
self.fail('EOS not received')
self.assertTrue(tags[0])

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ def create_proxy(config=None, mixer=None):
return DummyAudio.start(config, mixer).proxy()
# TODO: reset position on track change?
class DummyAudio(pykka.ThreadingActor):
def __init__(self, config=None, mixer=None):
@ -24,13 +25,15 @@ class DummyAudio(pykka.ThreadingActor):
self._position = 0
self._callback = None
self._uri = None
self._state_change_result = True
self._stream_changed = False
self._tags = {}
self._bad_uris = set()
def set_uri(self, uri):
assert self._uri is None, 'prepare change not called before set'
self._tags = {}
self._uri = uri
self._stream_changed = True
def set_appsrc(self, *args, **kwargs):
pass
@ -88,12 +91,15 @@ class DummyAudio(pykka.ThreadingActor):
if not self._uri:
return False
if self.state == audio.PlaybackState.STOPPED and self._uri:
audio.AudioListener.send('position_changed', position=0)
audio.AudioListener.send('stream_changed', uri=self._uri)
if new_state == audio.PlaybackState.STOPPED:
if new_state == audio.PlaybackState.STOPPED and self._uri:
self._stream_changed = True
self._uri = None
if self._uri is not None:
audio.AudioListener.send('position_changed', position=0)
if self._stream_changed:
self._stream_changed = False
audio.AudioListener.send('stream_changed', uri=self._uri)
old_state, self.state = self.state, new_state
@ -105,10 +111,10 @@ class DummyAudio(pykka.ThreadingActor):
self._tags['audio-codec'] = [u'fake info...']
audio.AudioListener.send('tags_changed', tags=['audio-codec'])
return self._state_change_result
return self._uri not in self._bad_uris
def trigger_fake_playback_failure(self):
self._state_change_result = False
def trigger_fake_playback_failure(self, uri):
self._bad_uris.add(uri)
def trigger_fake_tags_changed(self, tags):
self._tags.update(tags)

View File

@ -22,7 +22,10 @@ class DummyBackend(pykka.ThreadingActor, backend.Backend):
super(DummyBackend, self).__init__()
self.library = DummyLibraryProvider(backend=self)
self.playback = DummyPlaybackProvider(audio=audio, backend=self)
if audio:
self.playback = backend.PlaybackProvider(audio=audio, backend=self)
else:
self.playback = DummyPlaybackProvider(audio=audio, backend=self)
self.playlists = DummyPlaylistsProvider(backend=self)
self.uri_schemes = ['dummy']

View File

@ -111,17 +111,17 @@ class LocalPlaybackProviderTest(unittest.TestCase):
def test_play_mp3(self):
self.add_track('local:track:blank.mp3')
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
def test_play_ogg(self):
self.add_track('local:track:blank.ogg')
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
def test_play_flac(self):
self.add_track('local:track:blank.flac')
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
def test_play_uri_with_non_ascii_bytes(self):
@ -129,7 +129,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
# string will be decoded from ASCII to Unicode, which will crash on
# non-ASCII strings, like the bytestring the following URI decodes to.
self.add_track('local:track:12%20Doin%E2%80%99%20It%20Right.flac')
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
def test_initial_state_is_stopped(self):
@ -137,7 +137,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
def test_play_with_empty_playlist(self):
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.STOPPED)
def test_play_with_empty_playlist_return_value(self):
@ -146,7 +146,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_play_state(self):
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
@ -156,7 +156,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_play_track_state(self):
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
@ -165,139 +165,141 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_play_when_playing(self):
self.playback.play()
self.playback.play().get()
track = self.playback.get_current_track().get()
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(track)
@populate_tracklist
def test_play_when_paused(self):
self.playback.play()
self.playback.play().get()
track = self.playback.get_current_track().get()
self.playback.pause()
self.playback.play()
self.playback.pause().get()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
self.assert_current_track_is(track)
@populate_tracklist
def test_play_when_pause_after_next(self):
self.playback.play()
self.playback.next()
self.playback.next()
def test_play_when_paused_after_next(self):
self.playback.play().get()
self.playback.next().get()
self.playback.next().get()
track = self.playback.get_current_track().get()
self.playback.pause()
self.playback.play()
self.playback.pause().get()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
self.assert_current_track_is(track)
@populate_tracklist
def test_play_sets_current_track(self):
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
@populate_tracklist
def test_play_track_sets_current_track(self):
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.assert_current_track_is(self.tracks[-1])
@populate_tracklist
def test_play_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
return_values = [True, False]
self.backend.playback.play = lambda: return_values.pop()
self.playback.play()
uri = self.backend.playback.translate_uri(self.tracks[0].uri).get()
self.audio.trigger_fake_playback_failure(uri)
self.playback.play().get()
self.assert_current_track_is_not(self.tracks[0])
self.assert_current_track_is(self.tracks[1])
@populate_tracklist
def test_current_track_after_completed_playlist(self):
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.trigger_about_to_finish()
# EOS should have triggered
self.assert_state_is(PlaybackState.STOPPED)
self.assert_current_track_is(None)
self.playback.play(self.tl_tracks.get()[-1])
self.playback.next()
self.playback.play(self.tl_tracks.get()[-1]).get()
self.playback.next().get()
self.assert_state_is(PlaybackState.STOPPED)
self.assert_current_track_is(None)
@populate_tracklist
def test_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.playback.play().get()
self.playback.next().get()
self.playback.previous().get()
self.assert_current_track_is(self.tracks[0])
@populate_tracklist
def test_previous_more(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.playback.play().get() # At track 0
self.playback.next().get() # At track 1
self.playback.next().get() # At track 2
self.playback.previous().get() # At track 1
self.assert_current_track_is(self.tracks[1])
@populate_tracklist
def test_previous_return_value(self):
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
self.assertIsNone(self.playback.previous().get())
@populate_tracklist
def test_previous_does_not_trigger_playback(self):
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
self.playback.stop()
self.playback.previous()
self.playback.previous().get()
self.assert_state_is(PlaybackState.STOPPED)
@populate_tracklist
def test_previous_at_start_of_playlist(self):
self.playback.previous()
self.playback.previous().get()
self.assert_state_is(PlaybackState.STOPPED)
self.assert_current_track_is(None)
def test_previous_for_empty_playlist(self):
self.playback.previous()
self.playback.previous().get()
self.assert_state_is(PlaybackState.STOPPED)
self.assert_current_track_is(None)
@populate_tracklist
def test_previous_skips_to_previous_track_on_failure(self):
# If backend's play() returns False, it is a failure.
return_values = [True, False, True]
self.backend.playback.play = lambda: return_values.pop()
self.playback.play(self.tl_tracks.get()[2])
uri = self.backend.playback.translate_uri(self.tracks[1].uri).get()
self.audio.trigger_fake_playback_failure(uri)
self.playback.play(self.tl_tracks.get()[2]).get()
self.assert_current_track_is(self.tracks[2])
self.playback.previous()
self.playback.previous().get()
self.assert_current_track_is_not(self.tracks[1])
self.assert_current_track_is(self.tracks[0])
@populate_tracklist
def test_next(self):
self.playback.play()
self.playback.play().get()
old_track = self.playback.get_current_track().get()
old_position = self.tracklist.index().get()
self.playback.next()
self.playback.next().get()
self.assertEqual(self.tracklist.index().get(), old_position + 1)
self.assert_current_track_is_not(old_track)
@populate_tracklist
def test_next_return_value(self):
self.playback.play()
self.playback.play().get()
self.assertEqual(self.playback.next().get(), None)
@populate_tracklist
def test_next_does_not_trigger_playback(self):
self.playback.next()
self.playback.next().get()
self.assert_state_is(PlaybackState.STOPPED)
@populate_tracklist
def test_next_at_end_of_playlist(self):
self.playback.play()
self.playback.play().get()
for i, track in enumerate(self.tracks):
self.assert_state_is(PlaybackState.PLAYING)
@ -310,30 +312,31 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_next_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
self.playback.play().get()
for _ in self.tracks:
self.playback.next()
self.playback.next().get()
self.assert_current_track_is(None)
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
self.assert_current_track_is(self.tracks[0])
def test_next_for_empty_playlist(self):
self.playback.next()
self.playback.next().get()
self.assert_state_is(PlaybackState.STOPPED)
@populate_tracklist
def test_next_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
return_values = [True, False, True]
self.backend.playback.play = lambda: return_values.pop()
self.playback.play()
uri = self.backend.playback.translate_uri(self.tracks[1].uri).get()
self.audio.trigger_fake_playback_failure(uri)
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
self.playback.next()
self.playback.next().get()
self.assert_current_track_is_not(self.tracks[1])
self.assert_current_track_is(self.tracks[2])
@ -343,14 +346,14 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_next_track_during_play(self):
self.playback.play()
self.playback.play().get()
self.assert_next_tl_track_is(self.tl_tracks.get()[1])
@populate_tracklist
def test_next_track_after_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.playback.play().get()
self.playback.next().get()
self.playback.previous().get()
self.assert_next_tl_track_is(self.tl_tracks.get()[1])
def test_next_track_empty_playlist(self):
@ -358,17 +361,17 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
self.playback.play().get()
for _ in self.tl_tracks.get()[1:]:
self.playback.next()
self.playback.next().get()
self.assert_next_tl_track_is(None)
@populate_tracklist
def test_next_track_at_end_of_playlist_with_repeat(self):
self.tracklist.repeat = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.playback.next()
self.playback.next().get()
self.assert_next_tl_track_is(self.tl_tracks.get()[0])
@populate_tracklist
@ -382,17 +385,17 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_next_with_consume(self):
self.tracklist.consume = True
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
self.assertNotIn(self.tracks[0], self.tracklist.get_tracks().get())
@populate_tracklist
def test_next_with_single_and_repeat(self):
self.tracklist.single = True
self.tracklist.repeat = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
self.playback.next()
self.playback.next().get()
self.assert_current_track_is(self.tracks[1])
@populate_tracklist
@ -401,9 +404,9 @@ class LocalPlaybackProviderTest(unittest.TestCase):
shuffle_mock.side_effect = lambda tracks: tracks.reverse()
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[-1])
self.playback.next()
self.playback.next().get()
self.assert_current_track_is(self.tracks[-2])
@populate_tracklist
@ -434,7 +437,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track(self):
self.playback.play()
self.playback.play().get()
old_track = self.playback.get_current_track().get()
old_position = self.tracklist.index().get()
@ -447,7 +450,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_return_value(self):
self.playback.play()
self.playback.play().get()
self.assertEqual(self.trigger_about_to_finish(), None)
@populate_tracklist
@ -457,7 +460,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_at_end_of_playlist(self):
self.playback.play()
self.playback.play().get()
for i, track in enumerate(self.tracks):
self.assert_state_is(PlaybackState.PLAYING)
@ -470,7 +473,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
self.playback.play().get()
for _ in self.tracks:
self.trigger_about_to_finish()
@ -478,7 +481,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.assertEqual(self.playback.get_current_track().get(), None)
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
self.assert_current_track_is(self.tracks[0])
@ -486,12 +489,14 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.trigger_about_to_finish()
self.assert_state_is(PlaybackState.STOPPED)
# TODO: On about to finish does not handle skipping to next track yet.
@unittest.expectedFailure
@populate_tracklist
def test_end_of_track_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
return_values = [True, False, True]
self.backend.playback.play = lambda: return_values.pop()
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
self.trigger_about_to_finish()
self.assert_current_track_is_not(self.tracks[1])
@ -503,7 +508,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_track_during_play(self):
self.playback.play()
self.playback.play().get()
self.assert_next_tl_track_is(self.tl_tracks.get()[1])
@populate_tracklist
@ -518,7 +523,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.trigger_about_to_finish()
@ -527,7 +532,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.tracklist.repeat = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.trigger_about_to_finish()
@ -544,7 +549,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_track_with_consume(self):
self.tracklist.consume = True
self.playback.play()
self.playback.play().get()
self.trigger_about_to_finish()
self.assertNotIn(self.tracks[0], self.tracklist.get_tracks().get())
@ -554,7 +559,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
shuffle_mock.side_effect = lambda tracks: tracks.reverse()
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[-1])
self.trigger_about_to_finish()
self.assert_current_track_is(self.tracks[-2])
@ -592,21 +597,21 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_previous_track_after_play(self):
self.playback.play()
self.playback.play().get()
self.assert_previous_tl_track_is(None)
@populate_tracklist
def test_previous_track_after_next(self):
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
self.assert_previous_tl_track_is(self.tl_tracks.get()[0])
@populate_tracklist
def test_previous_track_after_previous(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.playback.play().get() # At track 0
self.playback.next().get() # At track 1
self.playback.next().get() # At track 2
self.playback.previous().get() # At track 1
self.assert_previous_tl_track_is(self.tl_tracks.get()[0])
def test_previous_track_empty_playlist(self):
@ -634,13 +639,13 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_current_track_during_play(self):
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
@populate_tracklist
def test_current_track_after_next(self):
self.playback.play()
self.playback.next()
self.playback.next().get()
self.assert_current_track_is(self.tracks[1])
@populate_tracklist
@ -649,18 +654,18 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_tracklist_position_during_play(self):
self.playback.play()
self.playback.play().get()
self.assert_current_track_index_is(0)
@populate_tracklist
def test_tracklist_position_after_next(self):
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
self.assert_current_track_index_is(1)
@populate_tracklist
def test_tracklist_position_at_end_of_playlist(self):
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.trigger_about_to_finish()
# EOS should have triggered
self.assert_current_track_index_is(None)
@ -672,7 +677,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_on_tracklist_change_when_playing(self):
self.playback.play()
self.playback.play().get()
current_track = self.playback.get_current_track().get()
self.tracklist.add([self.tracks[2]])
self.assert_state_is(PlaybackState.PLAYING)
@ -686,7 +691,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_on_tracklist_change_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
current_track = self.playback.get_current_track().get()
self.tracklist.add([self.tracks[2]])
@ -700,20 +705,20 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_pause_when_playing(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.assert_state_is(PlaybackState.PAUSED)
@populate_tracklist
def test_pause_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.playback.pause()
self.assert_state_is(PlaybackState.PAUSED)
@populate_tracklist
def test_pause_return_value(self):
self.playback.play()
self.playback.play().get()
self.assertIsNone(self.playback.pause().get())
@populate_tracklist
@ -723,27 +728,27 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_resume_when_playing(self):
self.playback.play()
self.playback.play().get()
self.playback.resume()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
def test_resume_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.playback.resume()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
def test_resume_return_value(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.assertIsNone(self.playback.resume().get())
@unittest.SkipTest # Uses sleep and might not work with LocalBackend
@populate_tracklist
def test_resume_continues_from_right_position(self):
self.playback.play()
self.playback.play().get()
time.sleep(0.2)
self.playback.pause()
self.playback.resume()
@ -756,7 +761,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_seek_when_stopped_updates_position(self):
self.playback.seek(1000)
self.playback.seek(1000).get()
position = self.playback.time_position
self.assertGreaterEqual(position, 990)
@ -764,31 +769,31 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.assertFalse(self.playback.seek(0).get())
def test_seek_on_empty_playlist_updates_position(self):
self.playback.seek(0)
self.playback.seek(0).get()
self.assert_state_is(PlaybackState.STOPPED)
@populate_tracklist
def test_seek_when_stopped_triggers_play(self):
self.playback.seek(0)
self.playback.seek(0).get()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
def test_seek_when_playing(self):
self.playback.play()
self.playback.play().get()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_when_playing_updates_position(self):
length = self.tracks[0].length
self.playback.play()
self.playback.seek(length - 1000)
self.playback.play().get()
self.playback.seek(length - 1000).get()
position = self.playback.get_time_position().get()
self.assertGreaterEqual(position, length - 1010)
@populate_tracklist
def test_seek_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
@ -797,7 +802,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_seek_when_paused_updates_position(self):
length = self.tracks[0].length
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.playback.seek(length - 1000)
position = self.playback.get_time_position().get()
@ -807,19 +812,19 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
self.playback.play()
self.playback.play().get()
result = self.playback.seek(self.tracks[0].length * 100)
self.assert_(not result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_beyond_end_of_song_jumps_to_next_song(self):
self.playback.play()
self.playback.seek(self.tracks[0].length * 100)
self.playback.play().get()
self.playback.seek(self.tracks[0].length * 100).get()
self.assert_current_track_is(self.tracks[1])
@populate_tracklist
def test_seek_beyond_end_of_song_for_last_track(self):
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.playback.seek(self.tracks[-1].length * 100)
self.assert_state_is(PlaybackState.STOPPED)
@ -830,19 +835,19 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_stop_when_playing(self):
self.playback.play()
self.playback.play().get()
self.playback.stop()
self.assert_state_is(PlaybackState.STOPPED)
@populate_tracklist
def test_stop_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause()
self.playback.stop()
self.assert_state_is(PlaybackState.STOPPED)
def test_stop_return_value(self):
self.playback.play()
self.playback.play().get()
self.assertIsNone(self.playback.stop().get())
def test_time_position_when_stopped(self):
@ -855,7 +860,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@unittest.SkipTest # Uses sleep and does might not work with LocalBackend
@populate_tracklist
def test_time_position_when_playing(self):
self.playback.play()
self.playback.play().get()
first = self.playback.time_position
time.sleep(1)
second = self.playback.time_position
@ -863,7 +868,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_time_position_when_paused(self):
self.playback.play()
self.playback.play().get()
self.playback.pause().get()
first = self.playback.get_time_position().get()
second = self.playback.get_time_position().get()
@ -872,13 +877,13 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_play_with_consume(self):
self.tracklist.consume = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
@populate_tracklist
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.tracklist.consume = True
self.playback.play()
self.playback.play().get()
for t in self.tracks:
self.trigger_about_to_finish()
@ -892,7 +897,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
shuffle_mock.side_effect = lambda tracks: tracks.reverse()
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[-1])
@populate_tracklist
@ -901,15 +906,15 @@ class LocalPlaybackProviderTest(unittest.TestCase):
shuffle_mock.side_effect = lambda tracks: tracks.reverse()
self.tracklist.random = True
self.playback.play()
self.playback.next()
self.playback.play().get()
self.playback.next().get()
current_track = self.playback.get_current_track().get()
self.playback.previous()
self.assert_current_track_is(current_track)
@populate_tracklist
def test_end_of_song_starts_next_track(self):
self.playback.play()
self.playback.play().get()
self.trigger_about_to_finish()
self.assert_current_track_is(self.tracks[1])
@ -917,7 +922,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
def test_end_of_song_with_single_and_repeat_starts_same(self):
self.tracklist.single = True
self.tracklist.repeat = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
self.trigger_about_to_finish()
self.assert_current_track_is(self.tracks[0])
@ -927,7 +932,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.tracklist.single = True
self.tracklist.repeat = True
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
current_track = self.playback.get_current_track().get()
self.trigger_about_to_finish()
self.assert_current_track_is(current_track)
@ -935,7 +940,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_song_with_single_stops(self):
self.tracklist.single = True
self.playback.play()
self.playback.play().get()
self.assert_current_track_is(self.tracks[0])
self.trigger_about_to_finish()
self.assert_current_track_is(None)
@ -946,7 +951,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
def test_end_of_song_with_single_and_random_stops(self):
self.tracklist.single = True
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
self.trigger_about_to_finish()
# EOS should have triggered
self.assert_current_track_is(None)
@ -954,7 +959,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_end_of_playlist_stops(self):
self.playback.play(self.tl_tracks.get()[-1])
self.playback.play(self.tl_tracks.get()[-1]).get()
self.trigger_about_to_finish()
# EOS should have triggered
self.assert_state_is(PlaybackState.STOPPED)
@ -971,15 +976,15 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_random_until_end_of_playlist(self):
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.playback.next()
self.playback.next().get()
self.assert_next_tl_track_is(None)
@populate_tracklist
def test_random_with_eot_until_end_of_playlist(self):
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.trigger_about_to_finish()
@ -988,9 +993,9 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks:
self.playback.next()
self.playback.next().get()
self.assert_next_tl_track_is_not(None)
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
@ -999,21 +1004,21 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_random_with_eot_until_end_of_playlist_and_play_from_start(self):
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks:
self.trigger_about_to_finish()
# EOS should have triggered
self.assert_eot_tl_track_is_not(None)
self.assert_state_is(PlaybackState.STOPPED)
self.playback.play()
self.playback.play().get()
self.assert_state_is(PlaybackState.PLAYING)
@populate_tracklist
def test_random_until_end_of_playlist_with_repeat(self):
self.tracklist.repeat = True
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
for _ in self.tracks[1:]:
self.playback.next()
self.assert_next_tl_track_is_not(None)
@ -1021,13 +1026,13 @@ class LocalPlaybackProviderTest(unittest.TestCase):
@populate_tracklist
def test_played_track_during_random_not_played_again(self):
self.tracklist.random = True
self.playback.play()
self.playback.play().get()
played = []
for _ in self.tracks:
track = self.playback.get_current_track().get()
self.assertNotIn(track, played)
played.append(track)
self.playback.next()
self.playback.next().get()
@populate_tracklist
@mock.patch('random.shuffle')
@ -1038,10 +1043,10 @@ class LocalPlaybackProviderTest(unittest.TestCase):
expected = self.tl_tracks.get()[::-1] + [None]
actual = []
self.playback.play()
self.playback.play().get()
self.tracklist.random = True
while self.playback.get_state().get() != PlaybackState.STOPPED:
self.playback.next()
self.playback.next().get()
actual.append(self.playback.get_current_tl_track().get())
if len(actual) > len(expected):
break

View File

@ -10,7 +10,7 @@ from mopidy import core
from mopidy.internal import deprecation
from mopidy.mpd import session, uri_mapper
from tests import dummy_backend, dummy_mixer
from tests import dummy_audio, dummy_backend, dummy_mixer
class MockConnection(mock.Mock):
@ -44,11 +44,13 @@ class BaseTestCase(unittest.TestCase):
self.mixer = dummy_mixer.create_proxy()
else:
self.mixer = None
self.backend = dummy_backend.create_proxy()
self.audio = dummy_audio.create_proxy()
self.backend = dummy_backend.create_proxy(audio=self.audio)
with deprecation.ignore():
self.core = core.Core.start(
self.get_config(),
audio=self.audio,
mixer=self.mixer,
backends=[self.backend]).proxy()

View File

@ -248,7 +248,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertEqual(self.core.playback.current_track.get(), None)
self.core.playback.play()
self.core.playback.next()
self.core.playback.stop()
self.core.playback.stop().get()
self.assertNotEqual(self.core.playback.current_track.get(), None)
self.send_request('play "-1"')
@ -266,6 +266,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_play_minus_is_ignored_if_playing(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
@ -278,6 +279,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_play_minus_one_resumes_if_paused(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
@ -312,8 +314,8 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
def test_playid_minus_1_plays_current_track_if_current_track_is_set(self):
self.assertEqual(self.core.playback.current_track.get(), None)
self.core.playback.play()
self.core.playback.next()
self.core.playback.play().get()
self.core.playback.next().get()
self.core.playback.stop()
self.assertNotEqual(None, self.core.playback.current_track.get())
@ -332,6 +334,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_playid_minus_is_ignored_if_playing(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
@ -344,6 +347,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_playid_minus_one_resumes_if_paused(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
@ -417,7 +421,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_seekcur_absolute_value(self):
self.core.playback.play()
self.core.playback.play().get()
self.send_request('seekcur "30"')
@ -425,7 +429,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_seekcur_positive_diff(self):
self.core.playback.play()
self.core.playback.play().get()
self.core.playback.seek(10000)
self.assertGreaterEqual(self.core.playback.time_position.get(), 10000)
@ -435,7 +439,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_seekcur_negative_diff(self):
self.core.playback.play()
self.core.playback.play().get()
self.core.playback.seek(30000)
self.assertGreaterEqual(self.core.playback.time_position.get(), 30000)

View File

@ -31,6 +31,7 @@ class IssueGH17RegressionTest(protocol.BaseTestCase):
Track(uri='dummy:e'),
Track(uri='dummy:f'),
]
self.audio.trigger_fake_playback_failure('dummy:error')
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks]).get()

View File

@ -16,7 +16,7 @@ class StatusHandlerTest(protocol.BaseTestCase):
self.backend.library.dummy_library = [track]
self.core.tracklist.add(uris=[track.uri]).get()
self.core.playback.play()
self.core.playback.play().get()
self.send_request('currentsong')
self.assertInResponse('file: dummy:/a')
self.assertInResponse('Time: 0')

View File

@ -11,7 +11,7 @@ from mopidy.models import Track
from mopidy.mpd import dispatcher
from mopidy.mpd.protocol import status
from tests import dummy_backend, dummy_mixer
from tests import dummy_audio, dummy_backend, dummy_mixer
PAUSED = PlaybackState.PAUSED
@ -31,12 +31,14 @@ class StatusHandlerTest(unittest.TestCase):
}
}
self.audio = dummy_audio.create_proxy()
self.mixer = dummy_mixer.create_proxy()
self.backend = dummy_backend.create_proxy()
self.backend = dummy_backend.create_proxy(audio=self.audio)
with deprecation.ignore():
self.core = core.Core.start(
config,
audio=self.audio,
mixer=self.mixer,
backends=[self.backend]).proxy()
@ -154,21 +156,21 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playlist_loaded_contains_song(self):
self.set_tracklist(Track(uri='dummy:/a'))
self.core.playback.play()
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('song', result)
self.assertGreaterEqual(int(result['song']), 0)
def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self):
self.set_tracklist(Track(uri='dummy:/a'))
self.core.playback.play()
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('songid', result)
self.assertEqual(int(result['songid']), 1)
def test_status_method_when_playing_contains_time_with_no_length(self):
self.set_tracklist(Track(uri='dummy:/a', length=None))
self.core.playback.play()
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('time', result)
(position, total) = result['time'].split(':')
@ -188,7 +190,7 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playing_contains_elapsed(self):
self.set_tracklist(Track(uri='dummy:/a', length=60000))
self.core.playback.play()
self.core.playback.play().get()
self.core.playback.pause()
self.core.playback.seek(59123)
result = dict(status.status(self.context))
@ -197,7 +199,7 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_starting_playing_contains_elapsed_zero(self):
self.set_tracklist(Track(uri='dummy:/a', length=10000))
self.core.playback.play()
self.core.playback.play().get()
self.core.playback.pause()
result = dict(status.status(self.context))
self.assertIn('elapsed', result)
@ -205,7 +207,7 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playing_contains_bitrate(self):
self.set_tracklist(Track(uri='dummy:/a', bitrate=3200))
self.core.playback.play()
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('bitrate', result)
self.assertEqual(int(result['bitrate']), 3200)