diff --git a/docs/api/backends.rst b/docs/api/backends.rst index 0db463f5..c59b5a30 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -16,6 +16,9 @@ The backend and its controllers Backend API =========== +.. automodule:: mopidy.backends + :synopsis: Backend interface. + .. note:: Currently this only documents the API that is available for use by @@ -26,8 +29,54 @@ Backend API generally just implement or override a few of these methods yourself to create a new backend with a complete feature set. -.. automodule:: mopidy.backends - :synopsis: Backend interface. +.. autoclass:: mopidy.backends.BaseBackend + :members: + :undoc-members: + + +Playback controller +------------------- + +Manages playback, with actions like play, pause, stop, next, previous, and +seek. + +.. autoclass:: mopidy.backends.BasePlaybackController + :members: + :undoc-members: + + +Mixer controller +---------------- + +Manages volume. See :class:`mopidy.mixers.BaseMixer`. + + +Current playlist controller +--------------------------- + +Manages everything related to the currently loaded playlist. + +.. autoclass:: mopidy.backends.BaseCurrentPlaylistController + :members: + :undoc-members: + + +Stored playlists controller +--------------------------- + +Manages stored playlist. + +.. autoclass:: mopidy.backends.BaseStoredPlaylistsController + :members: + :undoc-members: + + +Library controller +------------------ + +Manages the music library, e.g. searching for tracks to be added to a playlist. + +.. autoclass:: mopidy.backends.BaseLibraryController :members: :undoc-members: diff --git a/docs/changes.rst b/docs/changes.rst index f0953d7a..2673a7f0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,13 +4,51 @@ Changes This change log is used to track all major changes to Mopidy. -0.1.0a1 (unreleased) +0.1.0a2 (unreleased) ==================== -- Merged the ``gstreamer`` branch from Thomas Adamcik, bringing more than a - hundred new tests, several bugfixes and a new backend for playing music from - a local music archive. -- **[WIP]** Changed backend API for get/filter/find_exact/search. +- (nothing yet) + + +0.1.0a1 (2010-05-04) +==================== + +Since the previous release Mopidy has seen about 300 commits, more than 200 new +tests, a libspotify release, and major feature additions to Spotify. The new +releases from Spotify have lead to updates to our dependencies, and also to new +bugs in Mopidy. Thus, this is primarily a bugfix release, even though the not +yet finished work on a Gstreamer backend have been merged. + +All users are recommended to upgrade to 0.1.0a1, and should at the same time +ensure that they have the latest versions of our dependencies: Despotify r508 +if you are using DespotifyBackend, and pyspotify 1.1 with libspotify 0.0.4 if +you are using LibspotifyBackend. + +As always, report problems at our IRC channel or our issue tracker. Thanks! + +**Changes** + +- Backend API changes: + + - Removed ``backend.playback.volume`` wrapper. Use ``backend.mixer.volume`` + directly. + - Renamed ``backend.playback.playlist_position`` to + ``current_playlist_position`` to match naming of ``current_track``. + - Replaced ``get_by_id()`` with a more flexible ``get(**criteria)``. + +- Merged the ``gstreamer`` branch from Thomas Adamcik: + + - More than 200 new tests, and thus several bugfixes to existing code. + - Several new generic features, like shuffle, consume, and playlist repeat. + (Fixes: GH-3) + - **[Work in Progress]** A new backend for playing music from a local music + archive using the Gstreamer library. + +- Made :class:`mopidy.mixers.alsa.AlsaMixer` work on machines without a mixer + named "Master". +- Make :class:`mopidy.backends.DespotifyBackend` ignore local files in + playlists (feature added in Spotify 0.4.3). Reported by Richard Haugen Olsen. +- And much more. 0.1.0a0 (2010-03-27) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index eb53958c..5d8f16b9 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,7 +1,7 @@ from mopidy import settings as raw_settings def get_version(): - return u'0.1.0a1' + return u'0.1.0a2' def get_mpd_protocol_version(): return u'0.16.0' diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index a8741b56..9b08250b 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -14,6 +14,15 @@ __all__ = ['BaseBackend', 'BasePlaybackController', 'BaseLibraryController'] class BaseBackend(object): + """ + :param core_queue: a queue for sending messages to + :class:`mopidy.process.CoreProcess` + :type core_queue: :class:`multiprocessing.Queue` + :param mixer: either a mixer instance, or :class:`None` to use the mixer + defined in settings + :type mixer: :class:`mopidy.mixers.BaseMixer` or :class:`None` + """ + def __init__(self, core_queue=None, mixer=None): self.core_queue = core_queue if mixer is not None: @@ -22,7 +31,8 @@ class BaseBackend(object): self.mixer = get_class(settings.MIXER)() #: A :class:`multiprocessing.Queue` which can be used by e.g. library - #: callbacks to send messages to the core. + #: callbacks executing in other threads to send messages to the core + #: thread, so that action may be taken in the correct thread. core_queue = None #: The current playlist controller. An instance of @@ -73,14 +83,17 @@ class BaseCurrentPlaylistController(object): """ #: The current playlist version. Integer which is increased every time the - #: current playlist is changed. Is not reset before the MPD server is - #: restarted. + #: current playlist is changed. Is not reset before Mopidy is restarted. version = 0 def __init__(self, backend): self.backend = backend self._playlist = Playlist() + def destroy(self): + """Cleanup after component.""" + pass + @property def playlist(self): """The currently loaded :class:`mopidy.models.Playlist`.""" @@ -229,7 +242,7 @@ class BaseCurrentPlaylistController(object): self.playlist = self.playlist.with_(tracks=before+shuffled+after) def destroy(self): - """Cleanup after component""" + """Cleanup after component.""" pass @@ -242,6 +255,10 @@ class BaseLibraryController(object): def __init__(self, backend): self.backend = backend + def destroy(self): + """Cleanup after component.""" + pass + def find_exact(self, field, query): """ Find tracks in the library where ``field`` matches ``query`` exactly. @@ -256,11 +273,11 @@ class BaseLibraryController(object): def lookup(self, uri): """ - Lookup track with given URI. + Lookup track with given URI. Returns :class:`None` if not found. :param uri: track URI :type uri: string - :rtype: :class:`mopidy.models.Track` + :rtype: :class:`mopidy.models.Track` or :class:`None` """ raise NotImplementedError @@ -285,10 +302,6 @@ class BaseLibraryController(object): """ raise NotImplementedError - def destroy(self): - """Cleanup after component""" - pass - class BasePlaybackController(object): """ @@ -321,9 +334,10 @@ class BasePlaybackController(object): random = False #: :class:`True` - #: The current track is played repeatedly. + #: The current playlist is played repeatedly. To repeat a single track, + #: select both :attr:`repeat` and :attr:`single`. #: :class:`False` - #: The current track is played once. + #: The current playlist is played once. repeat = False #: :class:`True` @@ -340,6 +354,21 @@ class BasePlaybackController(object): self._play_time_accumulated = 0 self._play_time_started = None + def destroy(self): + """Cleanup after component.""" + pass + + @property + def current_playlist_position(self): + """The position of the current track in the current playlist.""" + if self.current_track is None: + return None + try: + return self.backend.current_playlist.playlist.tracks.index( + self.current_track) + except ValueError: + return None + @property def next_track(self): """ @@ -369,40 +398,30 @@ class BasePlaybackController(object): return tracks[0] if self.repeat: - return tracks[(self.playlist_position + 1) % len(tracks)] + return tracks[(self.current_playlist_position + 1) % len(tracks)] try: - return tracks[self.playlist_position + 1] + return tracks[self.current_playlist_position + 1] except IndexError: return None - @property - def playlist_position(self): - """The position in the current playlist.""" - if self.current_track is None: - return None - try: - return self.backend.current_playlist.playlist.tracks.index( - self.current_track) - except ValueError: - return None - @property def previous_track(self): """ The previous :class:`mopidy.models.Track` in the playlist. - For normal playback this is the previous track in the playlist. If random - and/or consume is enabled it should return the current track instead. + For normal playback this is the previous track in the playlist. If + random and/or consume is enabled it should return the current track + instead. """ if self.repeat or self.consume or self.random: return self.current_track - if self.current_track is None or self.playlist_position == 0: + if self.current_track is None or self.current_playlist_position == 0: return None return self.backend.current_playlist.playlist.tracks[ - self.playlist_position - 1] + self.current_playlist_position - 1] @property def state(self): @@ -464,23 +483,13 @@ class BasePlaybackController(object): def _current_wall_time(self): return int(time.time() * 1000) - @property - def volume(self): - # FIXME Shouldn't we just be using the backend mixer directly? ie can we - # remove this? - """ - The audio volume as an int in the range [0, 100]. - - :class:`None` if unknown. - """ - return self.backend.mixer.volume - - @volume.setter - def volume(self, volume): - self.backend.mixer.volume = volume - def end_of_track_callback(self): - """Tell the playback controller that end of track is reached.""" + """ + Tell the playback controller that end of track is reached. + + Typically called by :class:`mopidy.process.CoreProcess` after a message + from a library thread is received. + """ if self.next_track is not None: self.next() else: @@ -488,7 +497,12 @@ class BasePlaybackController(object): self.current_track = None def new_playlist_loaded_callback(self): - """Tell the playback controller that a new playlist has been loaded.""" + """ + Tell the playback controller that a new playlist has been loaded. + + Typically called by :class:`mopidy.process.CoreProcess` after a message + from a library thread is received. + """ self.current_track = None self._first_shuffle = True self._shuffled = [] @@ -611,10 +625,6 @@ class BasePlaybackController(object): def _stop(self): raise NotImplementedError - def destroy(self): - """Cleanup after component""" - pass - class BaseStoredPlaylistsController(object): """ @@ -626,6 +636,10 @@ class BaseStoredPlaylistsController(object): self.backend = backend self._playlists = [] + def destroy(self): + """Cleanup after component.""" + pass + @property def playlists(self): """List of :class:`mopidy.models.Playlist`.""" @@ -726,7 +740,3 @@ class BaseStoredPlaylistsController(object): :rtype: list of :class:`mopidy.models.Playlist` """ return filter(lambda p: query in p.name, self._playlists) - - def destroy(self): - """Cleanup after component""" - pass diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 8ea29d87..0fe9c506 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -137,6 +137,8 @@ class DespotifyTranslator(object): @classmethod def to_mopidy_track(cls, spotify_track): + if not spotify_track.has_meta_data(): + return None if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR: date = dt.date(spotify_track.year, 1, 1) else: @@ -158,7 +160,7 @@ class DespotifyTranslator(object): return Playlist( uri=spotify_playlist.get_uri(), name=spotify_playlist.name.decode(ENCODING), - tracks=[cls.to_mopidy_track(t) for t in spotify_playlist.tracks], + tracks=filter(None, [cls.to_mopidy_track(t) for t in spotify_playlist.tracks]), ) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index eecc514c..3783d825 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -47,13 +47,16 @@ class LibspotifyBackend(BaseBackend): self.stored_playlists = LibspotifyStoredPlaylistsController( backend=self) self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.audio_controller_class = kwargs.get( + 'audio_controller_class', AlsaController) self.spotify = self._connect() def _connect(self): logger.info(u'Connecting to Spotify') spotify = LibspotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, - core_queue=self.core_queue) + core_queue=self.core_queue, + audio_controller_class=self.audio_controller_class) spotify.start() return spotify @@ -175,12 +178,12 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) user_agent = 'Mopidy %s' % get_version() - def __init__(self, username, password, core_queue): + def __init__(self, username, password, core_queue, audio_controller_class): SpotifySessionManager.__init__(self, username, password) threading.Thread.__init__(self) self.core_queue = core_queue self.connected = threading.Event() - self.audio = AlsaController() + self.audio = audio_controller_class() self.session = None def run(self): diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index 12bc9193..9bcd8b6f 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -1,7 +1,10 @@ import alsaaudio +import logging from mopidy.mixers import BaseMixer +logger = logging.getLogger('mopidy.mixers.alsa') + class AlsaMixer(BaseMixer): """ Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control @@ -10,7 +13,16 @@ class AlsaMixer(BaseMixer): def __init__(self, *args, **kwargs): super(AlsaMixer, self).__init__(*args, **kwargs) - self._mixer = alsaaudio.Mixer() + # A mixer named 'Master' does not always exist, so we fall back to + # using 'PCM'. If this turns out to be a bad solution, we should make + # it possible to override with a setting. + self._mixer = None + for mixer_name in (u'Master', u'PCM'): + if mixer_name in alsaaudio.mixers(): + logger.info(u'Mixer in use: %s', mixer_name) + self._mixer = alsaaudio.Mixer(mixer_name) + break + assert self._mixer is not None def _get_volume(self): return self._mixer.getvolume()[0] diff --git a/mopidy/mpd/frontend.py b/mopidy/mpd/frontend.py index da3d9609..d7064370 100644 --- a/mopidy/mpd/frontend.py +++ b/mopidy/mpd/frontend.py @@ -679,8 +679,12 @@ class MpdFrontend(object): When listing the root directory, this currently returns the list of stored playlists. This behavior is deprecated; use ``listplaylists`` instead. + + MPD returns the same result, including both playlists and the files and + directories located at the root level, for both ``lsinfo``, ``lsinfo + ""``, and ``lsinfo "/"``. """ - if uri == u'/' or uri is None: + if uri is None or uri == u'/' or uri == u'': return self._stored_playlists_listplaylists() raise MpdNotImplemented # TODO @@ -835,6 +839,7 @@ class MpdFrontend(object): raise MpdAckError(e[0]) @handle_pattern(r'^play "(?P\d+)"$') + @handle_pattern(r'^play "(?P-1)"$') def _playback_playpos(self, songpos): """ *musicpd.org, playback section:* @@ -842,10 +847,17 @@ class MpdFrontend(object): ``play [SONGPOS]`` Begins playing the playlist at song number ``SONGPOS``. + + *MPoD:* + + - issues ``play "-1"`` after playlist replacement. """ songpos = int(songpos) try: - track = self.backend.current_playlist.playlist.tracks[songpos] + if songpos == -1: + track = self.backend.current_playlist.playlist.tracks[0] + else: + track = self.backend.current_playlist.playlist.tracks[songpos] return self.backend.playback.play(track) except IndexError: raise MpdAckError(u'Position out of bounds') @@ -954,7 +966,7 @@ class MpdFrontend(object): volume = 0 if volume > 100: volume = 100 - self.backend.playback.volume = volume + self.backend.mixer.volume = volume @handle_pattern(r'^single "(?P[01])"$') def _playback_single(self, state): @@ -1070,7 +1082,7 @@ class MpdFrontend(object): """ if self.backend.playback.current_track is not None: return self.backend.playback.current_track.mpd_format( - position=self.backend.playback.playlist_position) + position=self.backend.playback.current_playlist_position) @handle_pattern(r'^idle$') @handle_pattern(r'^idle (?P.+)$') @@ -1226,7 +1238,7 @@ class MpdFrontend(object): return self.__status_status_songpos() def __status_status_songpos(self): - return self.backend.playback.playlist_position + return self.backend.playback.current_playlist_position def __status_status_state(self): if self.backend.playback.state == self.backend.playback.PLAYING: @@ -1252,8 +1264,8 @@ class MpdFrontend(object): return self.backend.playback.current_track.length def __status_status_volume(self): - if self.backend.playback.volume is not None: - return self.backend.playback.volume + if self.backend.mixer.volume is not None: + return self.backend.mixer.volume else: return 0 diff --git a/tests/backends/base.py b/tests/backends/base.py index cc375b3a..2974b316 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -397,12 +397,13 @@ class BasePlaybackControllerTest(object): def test_next(self): self.playback.play() - old_position = self.playback.playlist_position + old_position = self.playback.current_playlist_position old_uri = self.playback.current_track.uri self.playback.next() - self.assertEqual(self.playback.playlist_position, old_position+1) + self.assertEqual(self.playback.current_playlist_position, + old_position+1) self.assertNotEqual(self.playback.current_track.uri, old_uri) @populate_playlist @@ -422,7 +423,7 @@ class BasePlaybackControllerTest(object): for i, track in enumerate(self.tracks): self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.current_track, track) - self.assertEqual(self.playback.playlist_position, i) + self.assertEqual(self.playback.current_playlist_position, i) self.playback.next() @@ -584,25 +585,25 @@ class BasePlaybackControllerTest(object): self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_playlist - def test_initial_playlist_position(self): - self.assertEqual(self.playback.playlist_position, None) + def test_initial_current_playlist_position(self): + self.assertEqual(self.playback.current_playlist_position, None) @populate_playlist - def test_playlist_position_during_play(self): + def test_current_playlist_position_during_play(self): self.playback.play() - self.assertEqual(self.playback.playlist_position, 0) + self.assertEqual(self.playback.current_playlist_position, 0) @populate_playlist - def test_playlist_position_after_next(self): + def test_current_playlist_position_after_next(self): self.playback.play() self.playback.next() - self.assertEqual(self.playback.playlist_position, 1) + self.assertEqual(self.playback.current_playlist_position, 1) @populate_playlist - def test_playlist_position_at_end_of_playlist(self): + def test_current_playlist_position_at_end_of_playlist(self): self.playback.play(self.tracks[-1]) self.playback.end_of_track_callback() - self.assertEqual(self.playback.playlist_position, None) + self.assertEqual(self.playback.current_playlist_position, None) def test_new_playlist_loaded_callback_gets_called(self): callback = self.playback.new_playlist_loaded_callback diff --git a/tests/backends/despotify_integrationtest.py b/tests/backends/despotify_integrationtest.py new file mode 100644 index 00000000..1d960f77 --- /dev/null +++ b/tests/backends/despotify_integrationtest.py @@ -0,0 +1,35 @@ +# TODO This integration test is work in progress. + +import unittest + +from mopidy.backends.despotify import DespotifyBackend +from mopidy.models import Track + +from tests.backends.base import * + +uris = [ + 'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt', + 'spotify:track:111sulhaZqgsnypz3MkiaW', + 'spotify:track:7t8oznvbeiAPMDRuK0R5ZT', +] + +class DespotifyCurrentPlaylistControllerTest( + BaseCurrentPlaylistControllerTest, unittest.TestCase): + tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)] + backend_class = DespotifyBackend + + +class DespotifyPlaybackControllerTest( + BasePlaybackControllerTest, unittest.TestCase): + tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)] + backend_class = DespotifyBackend + + +class DespotifyStoredPlaylistsControllerTest( + BaseStoredPlaylistsControllerTest, unittest.TestCase): + backend_class = DespotifyBackend + + +class DespotifyLibraryControllerTest( + BaseLibraryControllerTest, unittest.TestCase): + backend_class = DespotifyBackend diff --git a/tests/backends/libspotify_integrationtest.py b/tests/backends/libspotify_integrationtest.py new file mode 100644 index 00000000..6e4916f5 --- /dev/null +++ b/tests/backends/libspotify_integrationtest.py @@ -0,0 +1,42 @@ +# TODO This integration test is work in progress. + +import unittest + +from mopidy.backends.libspotify import LibspotifyBackend +from mopidy.models import Track + +from tests.backends.base import * + +uris = [ + 'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt', + 'spotify:track:111sulhaZqgsnypz3MkiaW', + 'spotify:track:7t8oznvbeiAPMDRuK0R5ZT', +] + +class LibspotifyCurrentPlaylistControllerTest( + BaseCurrentPlaylistControllerTest, unittest.TestCase): + tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)] + backend_class = LibspotifyBackend + + +class LibspotifyPlaybackControllerTest( + BasePlaybackControllerTest, unittest.TestCase): + tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)] + backend_class = LibspotifyBackend + + +class LibspotifyStoredPlaylistsControllerTest( + BaseStoredPlaylistsControllerTest, unittest.TestCase): + backend_class = LibspotifyBackend + + +class LibspotifyLibraryControllerTest( + BaseLibraryControllerTest, unittest.TestCase): + backend_class = LibspotifyBackend + + +# TODO Plug this into the backend under test to avoid music output during +# testing. +class DummyAudioController(object): + def music_delivery(self, *args, **kwargs): + pass diff --git a/tests/mpd/frontend_test.py b/tests/mpd/frontend_test.py index 686b7505..2ead2b1b 100644 --- a/tests/mpd/frontend_test.py +++ b/tests/mpd/frontend_test.py @@ -155,7 +155,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(int(result['volume']), 0) def test_status_method_contains_volume(self): - self.b.playback.volume = 17 + self.b.mixer.volume = 17 result = dict(self.h._status_status()) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 17) @@ -334,32 +334,32 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): def test_setvol_below_min(self): result = self.h.handle_request(u'setvol "-10"') self.assert_(u'OK' in result) - self.assertEqual(0, self.b.playback.volume) + self.assertEqual(0, self.b.mixer.volume) def test_setvol_min(self): result = self.h.handle_request(u'setvol "0"') self.assert_(u'OK' in result) - self.assertEqual(0, self.b.playback.volume) + self.assertEqual(0, self.b.mixer.volume) def test_setvol_middle(self): result = self.h.handle_request(u'setvol "50"') self.assert_(u'OK' in result) - self.assertEqual(50, self.b.playback.volume) + self.assertEqual(50, self.b.mixer.volume) def test_setvol_max(self): result = self.h.handle_request(u'setvol "100"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.playback.volume) + self.assertEqual(100, self.b.mixer.volume) def test_setvol_above_max(self): result = self.h.handle_request(u'setvol "110"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.playback.volume) + self.assertEqual(100, self.b.mixer.volume) def test_setvol_plus_is_ignored(self): result = self.h.handle_request(u'setvol "+10"') self.assert_(u'OK' in result) - self.assertEqual(10, self.b.playback.volume) + self.assertEqual(10, self.b.mixer.volume) def test_single_off(self): result = self.h.handle_request(u'single "0"') @@ -461,6 +461,14 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'ACK Position out of bounds' in result) self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) + def test_play_minus_one_plays_first_in_playlist(self): + track = Track(id=0) + self.b.current_playlist.load(Playlist(tracks=[track])) + 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) + def test_playid(self): self.b.current_playlist.load(Playlist(tracks=[Track(id=0)])) result = self.h.handle_request(u'playid "0"') @@ -964,9 +972,10 @@ class MusicDatabaseHandlerTest(unittest.TestCase): listplaylists_result = self.h.handle_request(u'listplaylists') self.assertEqual(lsinfo_result, listplaylists_result) - def test_lsinfo_with_path(self): - result = self.h.handle_request(u'lsinfo ""') - self.assert_(u'ACK Not implemented' in result) + def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): + lsinfo_result = self.h.handle_request(u'lsinfo ""') + listplaylists_result = self.h.handle_request(u'listplaylists') + self.assertEqual(lsinfo_result, listplaylists_result) def test_lsinfo_for_root_returns_same_as_listplaylists(self): lsinfo_result = self.h.handle_request(u'lsinfo "/"') diff --git a/tests/version_test.py b/tests/version_test.py index f6d6cfb3..d593f01c 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -9,7 +9,8 @@ class VersionTest(unittest.TestCase): def test_versions_can_be_strictly_ordered(self): self.assert_(SV(get_version()) > SV('0.1.0a0')) - self.assert_(SV(get_version()) < SV('0.1.0a2')) + self.assert_(SV(get_version()) > SV('0.1.0a1')) + self.assert_(SV(get_version()) < SV('0.1.0a3')) self.assert_(SV('0.1.0a1') < SV('0.1.0')) self.assert_(SV('0.1.0') < SV('0.1.1')) self.assert_(SV('0.1.1') < SV('0.2.0'))