merged jodal gstreamer with knutz3n singlerepeat branch

This commit is contained in:
Johannes Knutsen 2010-08-16 18:36:37 +02:00
commit 6905b81009
17 changed files with 186 additions and 122 deletions

View File

@ -35,12 +35,14 @@ greatly improved MPD client support.
- Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the
default mixer on all platforms.
- New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume.
- If failing to play a track, playback will skip to the next track.
- MPD frontend:
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
- Split gigantic protocol implementation into eleven modules.
- Search improvements, including support for multi-word search.
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty
or when a current track is set.
- Support ``plchanges "-1"`` to work better with MPDroid.
- Support ``pause`` without arguments to work better with MPDroid.
- Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and
@ -50,6 +52,7 @@ greatly improved MPD client support.
- Implement ``seek`` and ``seekid``.
- Fix ``playlistfind`` output so the correct song is played when playing
songs directly from search results in GMPC.
- Fix ``load`` so that one can append a playlist to the current playlist.
- Backends:
@ -73,7 +76,10 @@ greatly improved MPD client support.
- :meth:`mopidy.backends.base.BaseBackend()` now accepts an
``output_queue`` which it can use to send messages (i.e. audio data)
to the output process.
- :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()` now
appends to the existing playlist. Use
:meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you
want to clear it first.
0.1.0a3 (2010-08-03)

View File

@ -2,15 +2,21 @@
libspotify installation
***********************
We are working on a
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_ backend.
To use the libspotify backend you must install libspotify and
`pyspotify <http://github.com/winjer/pyspotify>`_.
Mopidy uses `libspotify
<http://developer.spotify.com/en/libspotify/overview/>`_ for playing music from
the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must
install libspotify and `pyspotify <http://github.com/winjer/pyspotify>`_.
.. warning::
This backend requires a Spotify premium account, and it requires you to get
an application key from Spotify before use.
This backend requires a `Spotify premium account
<http://www.spotify.com/no/get-spotify/premium/>`_.
.. note::
This product uses SPOTIFY CORE but is not endorsed, certified or otherwise
approved in any way by Spotify. Spotify is the registered trade mark of the
Spotify Group.
Installing libspotify on Linux

View File

@ -2,7 +2,7 @@
Licenses
********
For a list of contributors, see :ref:`authors`. For details on who have
For a list of contributors, see :doc:`authors`. For details on who have
contributed what, please refer to our git repository.
Source code license

View File

@ -66,10 +66,9 @@ class BaseCurrentPlaylistController(object):
def clear(self):
"""Clear the current playlist."""
self.backend.playback.stop()
self.backend.playback.current_cp_track = None
self._cp_tracks = []
self.version += 1
self.backend.playback.on_current_playlist_change()
def get(self, **criteria):
"""
@ -107,16 +106,15 @@ class BaseCurrentPlaylistController(object):
def load(self, tracks):
"""
Replace the tracks in the current playlist with the given tracks.
Append the given tracks to the current playlist.
:param tracks: tracks to load
:type tracks: list of :class:`mopidy.models.Track`
"""
self._cp_tracks = []
self.version += 1
for track in tracks:
self.add(track)
self.backend.playback.new_playlist_loaded_callback()
self.backend.playback.on_current_playlist_change()
def move(self, start, end, to_position):
"""
@ -148,6 +146,7 @@ class BaseCurrentPlaylistController(object):
to_position += 1
self._cp_tracks = new_cp_tracks
self.version += 1
self.backend.playback.on_current_playlist_change()
def remove(self, **criteria):
"""
@ -192,6 +191,7 @@ class BaseCurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
self.backend.playback.on_current_playlist_change()
def mpd_format(self, *args, **kwargs):
"""Not a part of the generic backend API."""

View File

@ -287,11 +287,9 @@ class BasePlaybackController(object):
Typically called by :class:`mopidy.process.CoreProcess` after a message
from a library thread is received.
"""
next_cp_track = self.cp_track_at_eot
if next_cp_track is not None and self._next(next_cp_track[1]):
original_cp_track = self.current_cp_track
self.current_cp_track = next_cp_track
self.state = self.PLAYING
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self.play(self.cp_track_at_eot)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
@ -302,48 +300,43 @@ class BasePlaybackController(object):
self.stop()
self.current_cp_track = None
def new_playlist_loaded_callback(self):
def on_current_playlist_change(self):
"""
Tell the playback controller that a new playlist has been loaded.
Tell the playback controller that the current playlist has changed.
Typically called by :class:`mopidy.process.CoreProcess` after a message
from a library thread is received.
Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
"""
self.current_cp_track = None
self._first_shuffle = True
self._shuffled = []
if self.state == self.PLAYING:
if len(self.backend.current_playlist.tracks) > 0:
self.play()
else:
self.stop()
elif self.state == self.PAUSED:
if not self.backend.current_playlist.cp_tracks:
self.stop()
self.current_cp_track = None
elif (self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
self.current_cp_track = None
self.stop()
def next(self):
"""Play the next track."""
original_cp_track = self.current_cp_track
if self.state == self.STOPPED:
return
elif self.cp_track_at_next is not None and self._next(self.next_track):
self.current_cp_track = self.cp_track_at_next
self.state = self.PLAYING
elif self.cp_track_at_next is None:
original_cp_track = self.current_cp_track
if self.cp_track_at_next:
self.play(self.cp_track_at_next)
else:
self.stop()
self.current_cp_track = None
# FIXME handle in play aswell?
# FIXME This should only be applied when reaching end of track, and not
# when pressing "next"
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
def _next(self, track):
return self._play(track)
def pause(self):
"""Pause playback."""
if self.state == self.PLAYING and self._pause():
@ -352,13 +345,16 @@ class BasePlaybackController(object):
def _pause(self):
raise NotImplementedError
def play(self, cp_track=None):
def play(self, cp_track=None, on_error_step=1):
"""
Play the given track or the currently active track.
:param cp_track: track to play
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
if cp_track is not None:
@ -368,13 +364,14 @@ class BasePlaybackController(object):
if self.state == self.PAUSED and cp_track is None:
self.resume()
elif cp_track is not None and self._play(cp_track[1]):
elif cp_track is not None:
self.current_cp_track = cp_track
self.state = self.PLAYING
# TODO Do something sensible when _play() returns False, like calling
# next(). Adding this todo instead of just implementing it as I want a
# test case first.
if not self._play(cp_track[1]):
if on_error_step == 1:
self.next()
elif on_error_step == -1:
self.previous()
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
@ -384,14 +381,11 @@ class BasePlaybackController(object):
def previous(self):
"""Play the previous track."""
if (self.previous_cp_track is not None
and self.state != self.STOPPED
and self._previous(self.previous_track)):
self.current_cp_track = self.previous_cp_track
self.state = self.PLAYING
def _previous(self, track):
return self._play(track)
if self.previous_cp_track is None:
return
if self.state == self.STOPPED:
return
self.play(self.previous_cp_track, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""

View File

@ -9,14 +9,18 @@ ENCODING = 'utf-8'
class LibspotifyBackend(BaseBackend):
"""
A Spotify backend which uses the official `libspotify library
<http://developer.spotify.com/en/libspotify/overview/>`_.
`pyspotify <http://github.com/winjer/pyspotify/>`_ is the Python bindings
for libspotify. It got no documentation, but multiple examples are
available. Like libspotify, pyspotify's calls are mostly asynchronous.
A `Spotify <http://www.spotify.com/>`_ backend which uses the official
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_
library and the `pyspotify <http://github.com/winjer/pyspotify/>`_ Python
bindings for libspotify.
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
.. note::
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
otherwise approved in any way by Spotify. Spotify is the registered
trade mark of the Spotify Group.
"""
# Imports inside methods are to prevent loading of __init__.py to fail on
@ -40,6 +44,7 @@ class LibspotifyBackend(BaseBackend):
def _connect(self):
from .session_manager import LibspotifySessionManager
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
logger.info(u'Connecting to Spotify')
spotify = LibspotifySessionManager(
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,

View File

@ -26,7 +26,7 @@ class LibspotifyPlaybackController(BasePlaybackController):
def _play(self, track):
self._set_output_state('READY')
if self.state == self.PLAYING:
self.stop()
self.backend.spotify.session.play(0)
if track.uri is None:
return False
try:

View File

@ -39,7 +39,7 @@ class LibspotifyTranslator(object):
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=320,
bitrate=160,
)
@classmethod

View File

@ -341,6 +341,7 @@ def swap(frontend, songpos1, songpos2):
tracks.insert(songpos1, song2)
del tracks[songpos2]
tracks.insert(songpos2, song1)
frontend.backend.current_playlist.clear()
frontend.backend.current_playlist.load(tracks)
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')

View File

@ -139,9 +139,7 @@ def playid(frontend, cpid):
cpid = int(cpid)
try:
if cpid == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
return frontend.backend.playback.play(cp_track)
@ -158,10 +156,11 @@ def playpos(frontend, songpos):
Begins playing the playlist at song number ``SONGPOS``.
*MPoD:*
*Many clients:*
- issues ``play "-1"`` after playlist replacement to start playback at
the first track.
- issue ``play "-1"`` after playlist replacement to start the current
track. If the current track is not set, start playback at the first
track.
*BitMPC:*
@ -170,15 +169,21 @@ def playpos(frontend, songpos):
songpos = int(songpos)
try:
if songpos == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
return frontend.backend.playback.play(cp_track)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
def _get_cp_track_for_play_minus_one(frontend):
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.playback.current_cp_track
if cp_track is None:
cp_track = frontend.backend.current_playlist.cp_tracks[0]
return cp_track
@handle_pattern(r'^previous$')
def previous(frontend):
"""

View File

@ -86,6 +86,10 @@ def load(frontend, name):
``load {NAME}``
Loads the playlist ``NAME.m3u`` from the playlist directory.
*Clarifications:*
- ``load`` appends the given playlist to the current playlist.
"""
matches = frontend.backend.stored_playlists.search(name)
if matches:
@ -139,9 +143,9 @@ def playlistmove(frontend, name, from_pos, to_pos):
*Clarifications:*
- The second argument is not a ``SONGID`` as used elsewhere in the
protocol documentation, but just the ``SONGPOS`` to move *from*,
i.e. ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
- The second argument is not a ``SONGID`` as used elsewhere in the protocol
documentation, but just the ``SONGPOS`` to move *from*, i.e.
``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
"""
raise MpdNotImplemented # TODO

View File

@ -91,12 +91,6 @@ class BaseCurrentPlaylistControllerTest(object):
self.controller.clear()
self.assertEqual(self.playback.state, self.playback.STOPPED)
def test_load(self):
tracks = []
self.assertNotEqual(id(tracks), id(self.controller.tracks))
self.controller.load(tracks)
self.assertEqual(tracks, self.controller.tracks)
def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a')
self.controller.load([Track(uri='z'), track, Track(uri='y')])
@ -136,10 +130,15 @@ class BaseCurrentPlaylistControllerTest(object):
self.controller.load([track1, track2, track3])
self.assertEqual(track2, self.controller.get(uri='b')[1])
@populate_playlist
def test_load_replaces_playlist(self):
self.backend.current_playlist.load([])
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
def test_load_appends_to_the_current_playlist(self):
self.controller.load([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.load([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_load_does_not_reset_version(self):
version = self.controller.version
@ -148,22 +147,17 @@ class BaseCurrentPlaylistControllerTest(object):
@populate_playlist
def test_load_preserves_playing_state(self):
tracks = self.controller.tracks
playback = self.playback
self.playback.play()
self.controller.load([tracks[1]])
self.assertEqual(playback.state, playback.PLAYING)
self.assertEqual(tracks[1], self.playback.current_track)
track = self.playback.current_track
self.controller.load(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.assertEqual(self.playback.current_track, track)
@populate_playlist
def test_load_preserves_stopped_state(self):
tracks = self.controller.tracks
playback = self.playback
self.controller.load([tracks[2]])
self.assertEqual(playback.state, playback.STOPPED)
self.assertEqual(None, self.playback.current_track)
self.controller.load(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_move_single(self):
@ -351,6 +345,14 @@ class BasePlaybackControllerTest(object):
self.playback.play(self.current_playlist.cp_tracks[-1])
self.assertEqual(self.playback.current_track, self.tracks[-1])
@populate_playlist
def test_play_skips_to_next_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[0]
self.playback.play()
self.assertNotEqual(self.playback.current_track, self.tracks[0])
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_current_track_after_completed_playlist(self):
self.playback.play(self.current_playlist.cp_tracks[-1])
@ -417,6 +419,16 @@ class BasePlaybackControllerTest(object):
self.playback.next()
self.assertEqual(self.playback.state, self.playback.STOPPED)
@populate_playlist
def test_next_skips_to_next_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.next()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_playlist
def test_previous(self):
self.playback.play()
@ -457,6 +469,16 @@ class BasePlaybackControllerTest(object):
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_previous_skips_to_previous_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[1]
self.playback.play(self.current_playlist.cp_tracks[2])
self.assertEqual(self.playback.current_track, self.tracks[2])
self.playback.previous()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_next_track_before_play(self):
self.assertEqual(self.playback.next_track, self.tracks[0])
@ -575,15 +597,15 @@ class BasePlaybackControllerTest(object):
self.playback.end_of_track_callback()
self.assertEqual(self.playback.current_playlist_position, None)
def test_new_playlist_loaded_callback_gets_called(self):
callback = self.playback.new_playlist_loaded_callback
def test_on_current_playlist_change_gets_called(self):
callback = self.playback.on_current_playlist_change
def wrapper():
wrapper.called = True
return callback()
wrapper.called = False
self.playback.new_playlist_loaded_callback = wrapper
self.playback.on_current_playlist_change = wrapper
self.backend.current_playlist.load([])
self.assert_(wrapper.called)
@ -608,27 +630,28 @@ class BasePlaybackControllerTest(object):
self.assert_(event.is_set())
@populate_playlist
def test_new_playlist_loaded_callback_when_playing(self):
def test_on_current_playlist_change_when_playing(self):
self.playback.play()
current_track = self.playback.current_track
self.backend.current_playlist.load([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.assertEqual(self.playback.current_track, self.tracks[2])
self.assertEqual(self.playback.current_track, current_track)
@populate_playlist
def test_new_playlist_loaded_callback_when_stopped(self):
def test_on_current_playlist_change_when_stopped(self):
current_track = self.playback.current_track
self.backend.current_playlist.load([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.next_track, self.tracks[2])
@populate_playlist
def test_new_playlist_loaded_callback_when_paused(self):
def test_on_current_playlist_change_when_paused(self):
self.playback.play()
self.playback.pause()
current_track = self.playback.current_track
self.backend.current_playlist.load([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.next_track, self.tracks[2])
self.assertEqual(self.playback.state, self.backend.playback.PAUSED)
self.assertEqual(self.playback.current_track, current_track)
@populate_playlist
def test_pause_when_stopped(self):
@ -915,7 +938,7 @@ class BasePlaybackControllerTest(object):
self.playback.random = True
self.assertEqual(self.playback.next_track, self.tracks[2])
self.backend.current_playlist.load(self.tracks[:1])
self.assertEqual(self.playback.next_track, self.tracks[0])
self.assertEqual(self.playback.next_track, self.tracks[1])
@populate_playlist
def test_played_track_during_random_not_played_again(self):
@ -927,13 +950,9 @@ class BasePlaybackControllerTest(object):
played.append(self.playback.current_track)
self.playback.next()
def test_playing_track_with_invalid_uri(self):
self.backend.current_playlist.load([Track(uri='foobar')])
self.playback.play()
self.assertEqual(self.playback.state, self.playback.STOPPED)
@populate_playlist
def test_playing_track_that_isnt_in_playlist(self):
test = lambda: self.playback.play(self.tracks[0])
test = lambda: self.playback.play((17, Track()))
self.assertRaises(AssertionError, test)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -225,13 +225,25 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
def test_play_minus_one_plays_first_in_playlist(self):
track = Track()
self.b.current_playlist.load([track])
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track, None)
self.b.current_playlist.load([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track, track)
self.assertEqual(self.b.playback.current_track.uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.load([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track, None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
self.assertNotEqual(self.b.playback.current_track, None)
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track.uri, 'b')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()
@ -246,13 +258,25 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_playid_minus_one_plays_first_in_playlist(self):
track = Track()
self.b.current_playlist.load([track])
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track, None)
self.b.current_playlist.load([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track, track)
self.assertEqual(self.b.playback.current_track.uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.load([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track, None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
self.assertNotEqual(self.b.playback.current_track, None)
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track.uri, 'b')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()