Merge branch 'master' into gstreamer
This commit is contained in:
commit
62dde4de74
@ -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:
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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<songpos>\d+)"$')
|
||||
@handle_pattern(r'^play "(?P<songpos>-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<state>[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<subsystems>.+)$')
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
35
tests/backends/despotify_integrationtest.py
Normal file
35
tests/backends/despotify_integrationtest.py
Normal file
@ -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
|
||||
42
tests/backends/libspotify_integrationtest.py
Normal file
42
tests/backends/libspotify_integrationtest.py
Normal file
@ -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
|
||||
@ -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 "/"')
|
||||
|
||||
@ -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'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user