Merge branch 'gstreamer' of git://github.com/jodal/mopidy into gstreamer-local-backend
Conflicts: mopidy/outputs/gstreamer.py
This commit is contained in:
commit
5c632116b8
@ -1,9 +0,0 @@
|
||||
Authors
|
||||
=======
|
||||
|
||||
Contributors to Mopidy in the order of appearance:
|
||||
|
||||
- Stein Magnus Jodal <stein.magnus@jodal.no>
|
||||
- Johannes Knutsen <johannes@knutseninfo.no>
|
||||
- Thomas Adamcik <adamcik@samfundet.no>
|
||||
- Kristian Klette <klette@klette.us>
|
||||
10
README.rst
10
README.rst
@ -2,10 +2,11 @@
|
||||
Mopidy
|
||||
******
|
||||
|
||||
Mopidy is an `Music Player Daemon <http://mpd.wikia.com/>`_ (MPD) server with a
|
||||
`Spotify <http://www.spotify.com/>`_ backend. Using a standard MPD client you
|
||||
can search for music in Spotify's vast archive, manage Spotify playlists and
|
||||
play music from Spotify.
|
||||
Mopidy is a music server which can play music from `Spotify
|
||||
<http://www.spotify.com/>`_ or from your local hard drive. To search for music
|
||||
in Spotify's vast archive, manage playlists, and play music, you can use most
|
||||
`MPD clients <http://mpd.wikia.com/>`_. MPD clients are available for most
|
||||
platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
|
||||
|
||||
To install Mopidy, check out
|
||||
`the installation docs <http://www.mopidy.com/docs/installation/>`_.
|
||||
@ -14,4 +15,3 @@ To install Mopidy, check out
|
||||
* `Source code <http://github.com/jodal/mopidy>`_
|
||||
* `Issue tracker <http://github.com/jodal/mopidy/issues>`_
|
||||
* IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
* `Presentation of Mopidy <http://www.slideshare.net/jodal/mopidy-3380516>`_
|
||||
|
||||
@ -1 +1,22 @@
|
||||
.. include:: ../AUTHORS.rst
|
||||
*******
|
||||
Authors
|
||||
*******
|
||||
|
||||
Contributors to Mopidy in the order of appearance:
|
||||
|
||||
- Stein Magnus Jodal <stein.magnus@jodal.no>
|
||||
- Johannes Knutsen <johannes@knutseninfo.no>
|
||||
- Thomas Adamcik <adamcik@samfundet.no>
|
||||
- Kristian Klette <klette@klette.us>
|
||||
|
||||
|
||||
Donations
|
||||
=========
|
||||
|
||||
If you already enjoy Mopidy, or don't enjoy it and want to help us making
|
||||
Mopidy better, you can `donate money <http://pledgie.com/campaigns/12647>`_ to
|
||||
Mopidy's development.
|
||||
|
||||
Any donated money will be used to cover service subscriptions (e.g. Spotify
|
||||
and Last.fm) and hardware devices (e.g. an used iPod Touch for testing Mopidy
|
||||
with MPod) needed for developing Mopidy.
|
||||
|
||||
@ -22,58 +22,99 @@ greatly improved MPD client support.
|
||||
<installation/libspotify>`.
|
||||
- If you used :mod:`mopidy.backends.libspotify` previously, pyspotify must be
|
||||
updated when updating to this release, to get working seek functionality.
|
||||
- The settings ``SERVER_HOSTNAME`` and ``SERVER_PORT`` has been renamed to
|
||||
``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT``.
|
||||
- :attr:`mopidy.settings.SERVER_HOSTNAME` and
|
||||
:attr:`mopidy.settings.SERVER_PORT` has been renamed to
|
||||
:attr:`mopidy.settings.MPD_SERVER_HOSTNAME` and
|
||||
:attr:`mopidy.settings.MPD_SERVER_PORT` to allow for multiple frontends in
|
||||
the future.
|
||||
|
||||
**Changes**
|
||||
|
||||
- Exit early if not Python >= 2.6, < 3.
|
||||
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
|
||||
the packages created by ``setup.py`` for i.e. PyPI.
|
||||
- A Spotify application key is now bundled with the source. The
|
||||
``SPOTIFY_LIB_APPKEY`` setting is thus removed.
|
||||
- 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.
|
||||
- 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
|
||||
``single`` without quotes to work better with BitMPC.
|
||||
- Fixed delete current playing track from playlist, which crashed several
|
||||
clients.
|
||||
- Fixed deletion of the currently playing track from the current playlist,
|
||||
which crashed several clients.
|
||||
- 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, and
|
||||
make it return the correct error message if the playlist is not found.
|
||||
- Support for single track repeat added. (Fixes: :issue:`4`)
|
||||
- Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming
|
||||
in backends.
|
||||
|
||||
- Backends:
|
||||
|
||||
- Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`.
|
||||
- Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained
|
||||
and the Libspotify backend is working much better.
|
||||
- Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming
|
||||
in backends.
|
||||
and the Libspotify backend is working much better. (Fixes: :issue:`9`,
|
||||
:issue:`10`, :issue:`13`)
|
||||
- A Spotify application key is now bundled with the source.
|
||||
:attr:`mopidy.settings.SPOTIFY_LIB_APPKEY` is thus removed.
|
||||
- If failing to play a track, playback will skip to the next track.
|
||||
|
||||
- Mixers:
|
||||
|
||||
- Added new :mod:`mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer`
|
||||
which now is the default mixer on all platforms.
|
||||
- New setting :attr:`mopidy.settings.MIXER_MAX_VOLUME` for capping the
|
||||
maximum output volume.
|
||||
|
||||
- Backend API:
|
||||
|
||||
- Relocate from :mod:`mopidy.backends` to :mod:`mopidy.backends.base`.
|
||||
- The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is
|
||||
no longer needed after the CPID refactoring.
|
||||
- :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.BaseLibraryController.find_exact()` now accepts
|
||||
keyword arguments of the form ``find_exact(artist=['foo'],
|
||||
album=['bar'])``.
|
||||
- :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts
|
||||
keyword arguments of the form ``search(artist=['foo', 'fighters'],
|
||||
album=['bar', 'grooves'])``.
|
||||
- :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.append()`
|
||||
replaces
|
||||
:meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()`. Use
|
||||
:meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you
|
||||
want to clear the current playlist.
|
||||
- The following fields in
|
||||
:class:`mopidy.backends.base.BasePlaybackController` has been renamed to
|
||||
reflect their relation to methods called on the controller:
|
||||
|
||||
- ``next_track`` to ``track_at_next``
|
||||
- ``next_cp_track`` to ``cp_track_at_next``
|
||||
- ``previous_track`` to ``track_at_previous``
|
||||
- ``previous_cp_track`` to ``cp_track_at_previous``
|
||||
|
||||
- :attr:`mopidy.backends.base.BasePlaybackController.track_at_eot` and
|
||||
:attr:`mopidy.backends.base.BasePlaybackController.cp_track_at_eot` has
|
||||
been added to better handle the difference between the user pressing next
|
||||
and the current track ending.
|
||||
- Rename
|
||||
:meth:`mopidy.backends.base.BasePlaybackController.new_playlist_loaded_callback()`
|
||||
to
|
||||
:meth:`mopidy.backends.base.BasePlaybackController.on_current_playlist_change()`.
|
||||
- Rename
|
||||
:meth:`mopidy.backends.base.BasePlaybackController.end_of_track_callback()`
|
||||
to :meth:`mopidy.backends.base.BasePlaybackController.on_end_of_track()`.
|
||||
- Remove :meth:`mopidy.backends.base.BaseStoredPlaylistsController.search()`
|
||||
since it was barely used, untested, and we got no use case for non-exact
|
||||
search in stored playlists yet. Use
|
||||
:meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead.
|
||||
|
||||
|
||||
0.1.0a3 (2010-08-03)
|
||||
|
||||
@ -28,35 +28,51 @@ released when we reach the other goal.
|
||||
possible to have both Spotify tracks and local tracks in the same playlist.
|
||||
|
||||
|
||||
Stuff we really want to do, but just not right now
|
||||
==================================================
|
||||
Stuff we want to do, but not right now, and maybe never
|
||||
=======================================================
|
||||
|
||||
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies
|
||||
for all our dependencies and Mopidy itself to make OS X installation a
|
||||
breeze. See `Homebrew's issue #1612
|
||||
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
|
||||
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all our
|
||||
dependencies and Mopidy itself (hosted in our own Debian repo until we get
|
||||
stuff into the various distros) to make Debian/Ubuntu installation a breeze.
|
||||
- Run frontend tests against a real MPD server to ensure we are in sync.
|
||||
- Start working with MPD client maintainers to get rid of weird assumptions
|
||||
like only searching for first two letters and doing the rest of the filtering
|
||||
locally in the client, etc.
|
||||
- Packaging and distribution:
|
||||
|
||||
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_
|
||||
recipies for all our dependencies and Mopidy itself to make OS X
|
||||
installation a breeze. See `Homebrew's issue #1612
|
||||
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
|
||||
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all
|
||||
our dependencies and Mopidy itself (hosted in our own Debian repo until we
|
||||
get stuff into the various distros) to make Debian/Ubuntu installation a
|
||||
breeze.
|
||||
|
||||
Crazy stuff we had to write down somewhere
|
||||
==========================================
|
||||
- Compatability:
|
||||
|
||||
- Add an `XMMS2 <http://www.xmms2.org/>`_ frontend, so Mopidy can serve XMMS2
|
||||
clients.
|
||||
- Add support for serving the music as an `Icecast <http://www.icecast.org/>`_
|
||||
stream instead of playing it locally.
|
||||
- Integrate with `Squeezebox <http://www.logitechsqueezebox.com/>`_ in some
|
||||
way.
|
||||
- AirPort Express support, like in
|
||||
`PulseAudio <http://git.0pointer.de/?p=pulseaudio.git;a=blob;f=src/modules/raop/raop_client.c;hb=HEAD>`_.
|
||||
- DNLA and/or UPnP support. Maybe using
|
||||
`Coherence <http://coherence-project.org/>`_.
|
||||
- `Media Player Remote Interfacing Specification
|
||||
<http://en.wikipedia.org/wiki/Media_Player_Remote_Interfacing_Specification>`_
|
||||
support.
|
||||
- Run frontend tests against a real MPD server to ensure we are in sync.
|
||||
- Start working with MPD client maintainers to get rid of weird assumptions
|
||||
like only searching for first two letters and doing the rest of the
|
||||
filtering locally in the client (:issue:`1`), etc.
|
||||
|
||||
- Backends:
|
||||
|
||||
- `Last.fm <http://www.last.fm/api>`_
|
||||
- `WIMP <http://twitter.com/wimp/status/8975885632>`_
|
||||
- DNLA/UPnP to Mopidy can play music from other DNLA MediaServers.
|
||||
|
||||
- Frontends:
|
||||
|
||||
- D-Bus/`MPRIS <http://www.mpris.org/>`_
|
||||
- REST/JSON web service with a jQuery client as example application. Maybe
|
||||
based upon `Tornado <http://github.com/facebook/tornado>`_ and `jQuery
|
||||
Mobile <http://jquerymobile.com/>`_.
|
||||
- DNLA/UPnP to Mopidy can be controlled from i.e. TVs.
|
||||
- `XMMS2 <http://www.xmms2.org/>`_
|
||||
- LIRC frontend for controlling Mopidy with a remote.
|
||||
|
||||
- Mixers:
|
||||
|
||||
- LIRC mixer for controlling arbitrary amplifiers remotely.
|
||||
|
||||
- Audio streaming:
|
||||
|
||||
- Ogg Vorbis/MP3 audio stream over HTTP, to MPD clients, `Squeezeboxes
|
||||
<http://www.logitechsqueezebox.com/>`_, etc.
|
||||
- Feed audio to an `Icecast <http://www.icecast.org/>`_ server.
|
||||
- Stream to AirPort Express using `RAOP
|
||||
<http://en.wikipedia.org/wiki/Remote_Audio_Output_Protocol>`_.
|
||||
|
||||
@ -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
|
||||
@ -59,6 +65,8 @@ Install pyspotify's dependencies. At Debian/Ubuntu systems::
|
||||
|
||||
sudo aptitude install python-dev
|
||||
|
||||
In OS X no additional dependencies are needed.
|
||||
|
||||
Check out the pyspotify code, and install it::
|
||||
|
||||
git clone git://github.com/jodal/pyspotify.git
|
||||
|
||||
@ -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
|
||||
|
||||
@ -64,12 +64,23 @@ class BaseCurrentPlaylistController(object):
|
||||
self.version += 1
|
||||
return cp_track
|
||||
|
||||
def append(self, tracks):
|
||||
"""
|
||||
Append the given tracks to the current playlist.
|
||||
|
||||
:param tracks: tracks to append
|
||||
:type tracks: list of :class:`mopidy.models.Track`
|
||||
"""
|
||||
self.version += 1
|
||||
for track in tracks:
|
||||
self.add(track)
|
||||
self.backend.playback.on_current_playlist_change()
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -105,19 +116,6 @@ class BaseCurrentPlaylistController(object):
|
||||
else:
|
||||
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
|
||||
|
||||
def load(self, tracks):
|
||||
"""
|
||||
Replace the tracks in the current playlist with the given tracks.
|
||||
|
||||
: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()
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
Move the tracks in the slice ``[start:end]`` to ``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."""
|
||||
|
||||
@ -25,7 +25,7 @@ class BasePlaybackController(object):
|
||||
#: Tracks are not removed from the playlist.
|
||||
consume = False
|
||||
|
||||
#: The currently playing or selected track
|
||||
#: The currently playing or selected track.
|
||||
#:
|
||||
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
||||
#: :class:`None`.
|
||||
@ -45,7 +45,8 @@ class BasePlaybackController(object):
|
||||
repeat = False
|
||||
|
||||
#: :class:`True`
|
||||
#: Playback is stopped after current song, unless in repeat mode.
|
||||
#: Playback is stopped after current song, unless in :attr:`repeat`
|
||||
#: mode.
|
||||
#: :class:`False`
|
||||
#: Playback continues after current song.
|
||||
single = False
|
||||
@ -59,19 +60,32 @@ class BasePlaybackController(object):
|
||||
self._play_time_started = None
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
"""
|
||||
Cleanup after component.
|
||||
|
||||
May be overridden by subclasses.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_cpid(self, cp_track):
|
||||
if cp_track is None:
|
||||
return None
|
||||
return cp_track[0]
|
||||
|
||||
def _get_track(self, cp_track):
|
||||
if cp_track is None:
|
||||
return None
|
||||
return cp_track[1]
|
||||
|
||||
@property
|
||||
def current_cpid(self):
|
||||
"""
|
||||
The CPID (current playlist ID) of :attr:`current_track`.
|
||||
The CPID (current playlist ID) of the currently playing or selected
|
||||
track.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[0]
|
||||
return self._get_cpid(self.current_cp_track)
|
||||
|
||||
@property
|
||||
def current_track(self):
|
||||
@ -80,13 +94,15 @@ class BasePlaybackController(object):
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[1]
|
||||
return self._get_track(self.current_cp_track)
|
||||
|
||||
@property
|
||||
def current_playlist_position(self):
|
||||
"""The position of the current track in the current playlist."""
|
||||
"""
|
||||
The position of the current track in the current playlist.
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
try:
|
||||
@ -96,24 +112,71 @@ class BasePlaybackController(object):
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_track(self):
|
||||
def track_at_eot(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
The track that will be played at the end of the current track.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`next_cp_track` for
|
||||
convenience.
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_eot` for convenience.
|
||||
"""
|
||||
next_cp_track = self.next_cp_track
|
||||
if next_cp_track is None:
|
||||
return None
|
||||
return next_cp_track[1]
|
||||
return self._get_track(self.cp_track_at_eot)
|
||||
|
||||
@property
|
||||
def next_cp_track(self):
|
||||
def cp_track_at_eot(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
The track that will be played at the end of the current track.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
Not necessarily the same track as :attr:`cp_track_at_next`.
|
||||
"""
|
||||
cp_tracks = self.backend.current_playlist.cp_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
return None
|
||||
|
||||
if self.random and not self._shuffled:
|
||||
if self.repeat or self._first_shuffle:
|
||||
logger.debug('Shuffling tracks')
|
||||
self._shuffled = cp_tracks
|
||||
random.shuffle(self._shuffled)
|
||||
self._first_shuffle = False
|
||||
|
||||
if self._shuffled:
|
||||
return self._shuffled[0]
|
||||
|
||||
if self.current_cp_track is None:
|
||||
return cp_tracks[0]
|
||||
|
||||
if self.repeat and self.single:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position) % len(cp_tracks)]
|
||||
|
||||
if self.repeat:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
|
||||
try:
|
||||
return cp_tracks[self.current_playlist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def track_at_next(self):
|
||||
"""
|
||||
The track that will be played if calling :meth:`next()`.
|
||||
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_next` for convenience.
|
||||
"""
|
||||
return self._get_track(self.cp_track_at_next)
|
||||
|
||||
@property
|
||||
def cp_track_at_next(self):
|
||||
"""
|
||||
The track that will be played if calling :meth:`next()`.
|
||||
|
||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the next track in the playlist. If repeat
|
||||
is enabled the next track can loop around the playlist. When random is
|
||||
@ -148,22 +211,19 @@ class BasePlaybackController(object):
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_track(self):
|
||||
def track_at_previous(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
The track that will be played if calling :meth:`previous()`.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`previous_cp_track`
|
||||
for convenience.
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_previous` for convenience.
|
||||
"""
|
||||
previous_cp_track = self.previous_cp_track
|
||||
if previous_cp_track is None:
|
||||
return None
|
||||
return previous_cp_track[1]
|
||||
return self._get_track(self.cp_track_at_previous)
|
||||
|
||||
@property
|
||||
def previous_cp_track(self):
|
||||
def cp_track_at_previous(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
The track that will be played if calling :meth:`previous()`.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
@ -240,109 +300,122 @@ class BasePlaybackController(object):
|
||||
def _current_wall_time(self):
|
||||
return int(time.time() * 1000)
|
||||
|
||||
def end_of_track_callback(self):
|
||||
def on_end_of_track(self):
|
||||
"""
|
||||
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_cp_track is not None:
|
||||
self.next()
|
||||
original_cp_track = self.current_cp_track
|
||||
if self.cp_track_at_eot:
|
||||
self.play(self.cp_track_at_eot)
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
else:
|
||||
self.stop()
|
||||
self.current_cp_track = None
|
||||
|
||||
def new_playlist_loaded_callback(self):
|
||||
"""
|
||||
Tell the playback controller that a new playlist has been loaded.
|
||||
if self.consume:
|
||||
self.backend.current_playlist.remove(cpid=original_cp_track[0])
|
||||
|
||||
Typically called by :class:`mopidy.process.CoreProcess` after a message
|
||||
from a library thread is received.
|
||||
def on_current_playlist_change(self):
|
||||
"""
|
||||
Tell the playback controller that the current playlist has changed.
|
||||
|
||||
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.next_cp_track is not None and self._next(self.next_track):
|
||||
self.current_cp_track = self.next_cp_track
|
||||
self.state = self.PLAYING
|
||||
elif self.next_cp_track is None:
|
||||
|
||||
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?
|
||||
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():
|
||||
self.state = self.PAUSED
|
||||
|
||||
def _pause(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's pause
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
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.
|
||||
Play the given track, or if the given track is :class:`None`, play 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:
|
||||
assert cp_track in self.backend.current_playlist.cp_tracks
|
||||
elif not self.current_cp_track:
|
||||
cp_track = self.next_cp_track
|
||||
cp_track = self.cp_track_at_next
|
||||
|
||||
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)
|
||||
|
||||
def _play(self, track):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's play
|
||||
functionality here.
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
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.cp_track_at_previous is None:
|
||||
return
|
||||
if self.state == self.STOPPED:
|
||||
return
|
||||
self.play(self.cp_track_at_previous, on_error_step=-1)
|
||||
|
||||
def resume(self):
|
||||
"""If paused, resume playing the current track."""
|
||||
@ -350,6 +423,12 @@ class BasePlaybackController(object):
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _resume(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's resume
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
@ -376,6 +455,14 @@ class BasePlaybackController(object):
|
||||
self._seek(time_position)
|
||||
|
||||
def _seek(self, time_position):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's seek
|
||||
functionality here.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
@ -384,4 +471,10 @@ class BasePlaybackController(object):
|
||||
self.state = self.STOPPED
|
||||
|
||||
def _stop(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's stop
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -107,13 +107,3 @@ class BaseStoredPlaylistsController(object):
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, query):
|
||||
"""
|
||||
Search for playlists whose name contains ``query``.
|
||||
|
||||
:param query: query to search for
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return filter(lambda p: query in p.name, self._playlists)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -39,7 +39,7 @@ class LibspotifyTranslator(object):
|
||||
track_no=spotify_track.index(),
|
||||
date=date,
|
||||
length=spotify_track.duration(),
|
||||
bitrate=320,
|
||||
bitrate=160,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -11,11 +11,15 @@ def add(frontend, uri):
|
||||
Adds the file ``URI`` to the playlist (directories add recursively).
|
||||
``URI`` can also be a single file.
|
||||
"""
|
||||
track = frontend.backend.library.lookup(uri)
|
||||
if track is None:
|
||||
raise MpdNoExistError(
|
||||
u'directory or file not found', command=u'add')
|
||||
frontend.backend.current_playlist.add(track)
|
||||
for handler_prefix in frontend.backend.uri_handlers:
|
||||
if uri.startswith(handler_prefix):
|
||||
track = frontend.backend.library.lookup(uri)
|
||||
if track is not None:
|
||||
frontend.backend.current_playlist.add(track)
|
||||
return
|
||||
|
||||
raise MpdNoExistError(
|
||||
u'directory or file not found', command=u'add')
|
||||
|
||||
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
def addid(frontend, uri, songpos=None):
|
||||
@ -341,7 +345,8 @@ def swap(frontend, songpos1, songpos2):
|
||||
tracks.insert(songpos1, song2)
|
||||
del tracks[songpos2]
|
||||
tracks.insert(songpos2, song1)
|
||||
frontend.backend.current_playlist.load(tracks)
|
||||
frontend.backend.current_playlist.clear()
|
||||
frontend.backend.current_playlist.append(tracks)
|
||||
|
||||
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||
def swapid(frontend, cpid1, cpid2):
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -86,10 +86,16 @@ 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:
|
||||
frontend.backend.current_playlist.load(matches[0].tracks)
|
||||
try:
|
||||
playlist = frontend.backend.stored_playlists.get(name=name)
|
||||
frontend.backend.current_playlist.append(playlist.tracks)
|
||||
except LookupError as e:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'load')
|
||||
|
||||
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def playlistadd(frontend, name, uri):
|
||||
@ -139,9 +145,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
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ class CoreProcess(BaseProcess):
|
||||
connection = unpickle_connection(message['reply_to'])
|
||||
connection.send(response)
|
||||
elif message['command'] == 'end_of_track':
|
||||
self.backend.playback.end_of_track_callback()
|
||||
self.backend.playback.on_end_of_track()
|
||||
elif message['command'] == 'stop_playback':
|
||||
self.backend.playback.stop()
|
||||
elif message['command'] == 'set_stored_playlists':
|
||||
|
||||
@ -96,12 +96,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')])
|
||||
@ -141,10 +135,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
|
||||
@ -153,22 +152,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):
|
||||
@ -371,6 +365,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])
|
||||
@ -437,6 +439,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()
|
||||
@ -477,6 +489,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])
|
||||
@ -595,15 +617,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)
|
||||
@ -616,27 +638,28 @@ class BasePlaybackControllerTest(object):
|
||||
self.assertEqual('end_of_track', message['command'])
|
||||
|
||||
@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):
|
||||
@ -817,14 +840,29 @@ class BasePlaybackControllerTest(object):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
self.playback.next()
|
||||
self.assert_(self.tracks[0] in self.backend.current_playlist.tracks)
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_track_with_consume(self):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
self.playback.end_of_track_callback()
|
||||
self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks)
|
||||
|
||||
@populate_playlist
|
||||
def test_next_with_single_and_repeat(self):
|
||||
self.playback.single = True
|
||||
self.playback.repeat = True
|
||||
self.playback.play()
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.current_track, self.tracks[1])
|
||||
|
||||
@populate_playlist
|
||||
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
for i in range(len(self.backend.current_playlist.tracks)):
|
||||
self.playback.next()
|
||||
self.playback.end_of_track_callback()
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
|
||||
|
||||
@populate_playlist
|
||||
@ -859,6 +897,14 @@ class BasePlaybackControllerTest(object):
|
||||
self.playback.end_of_track_callback()
|
||||
self.assertEqual(self.playback.current_track, self.tracks[1])
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_song_with_single_and_repeat_starts_same(self):
|
||||
self.playback.single = True
|
||||
self.playback.repeat = True
|
||||
self.playback.play()
|
||||
self.playback.end_of_track_callback()
|
||||
self.assertEqual(self.playback.current_track, self.tracks[0])
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_playlist_stops(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
@ -907,7 +953,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):
|
||||
@ -919,13 +965,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.
@ -13,7 +13,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def test_add(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'add "dummy://foo"')
|
||||
@ -22,6 +22,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_add_with_uri_not_found_in_library_should_not_call_lookup(self):
|
||||
self.b.library.lookup = lambda uri: self.fail("Shouldn't run")
|
||||
result = self.h.handle_request(u'add "foo"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [50@0] {add} directory or file not found')
|
||||
|
||||
def test_add_with_uri_not_found_in_library_should_ack(self):
|
||||
result = self.h.handle_request(u'add "dummy://foo"')
|
||||
self.assertEqual(result[0],
|
||||
@ -30,7 +36,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def test_addid_without_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo"')
|
||||
@ -43,7 +49,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def test_addid_with_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo" "3"')
|
||||
@ -56,7 +62,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def test_addid_with_songpos_out_of_bounds_should_ack(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo" "6"')
|
||||
@ -67,7 +73,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
|
||||
|
||||
def test_clear(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'clear')
|
||||
@ -76,7 +82,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_songpos(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'delete "%d"' %
|
||||
@ -85,7 +91,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_songpos_out_of_bounds(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'delete "5"')
|
||||
@ -93,7 +99,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
|
||||
|
||||
def test_delete_open_range(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'delete "1:"')
|
||||
@ -101,7 +107,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_closed_range(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'delete "1:3"')
|
||||
@ -109,7 +115,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_range_out_of_bounds(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
result = self.h.handle_request(u'delete "5:7"')
|
||||
@ -117,21 +123,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
|
||||
|
||||
def test_deleteid(self):
|
||||
self.b.current_playlist.load([Track(), Track()])
|
||||
self.b.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 2)
|
||||
result = self.h.handle_request(u'deleteid "2"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_deleteid_does_not_exist(self):
|
||||
self.b.current_playlist.load([Track(), Track()])
|
||||
self.b.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 2)
|
||||
result = self.h.handle_request(u'deleteid "12345"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 2)
|
||||
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
|
||||
|
||||
def test_move_songpos(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -145,7 +151,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_move_open_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -159,7 +165,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_move_closed_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -173,7 +179,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_moveid(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -208,7 +214,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistfind_by_filename_in_current_playlist(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(uri='file:///exists')])
|
||||
result = self.h.handle_request(
|
||||
u'playlistfind filename "file:///exists"')
|
||||
@ -218,14 +224,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_without_songid(self):
|
||||
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_with_songid(self):
|
||||
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid "2"')
|
||||
self.assert_(u'Title: a' not in result)
|
||||
self.assert_(u'Id: 1' not in result)
|
||||
@ -234,12 +240,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_with_not_existing_songid_fails(self):
|
||||
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid "25"')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
|
||||
|
||||
def test_playlistinfo_without_songpos_or_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -253,7 +259,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_songpos(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -272,7 +278,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(result1, result2)
|
||||
|
||||
def test_playlistinfo_with_open_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -286,7 +292,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_closed_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -316,7 +322,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_plchanges(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges "0"')
|
||||
self.assert_(u'Title: a' in result)
|
||||
@ -325,7 +331,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchanges_with_minus_one_returns_entire_playlist(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges "-1"')
|
||||
self.assert_(u'Title: a' in result)
|
||||
@ -334,7 +340,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchanges_without_quotes_works(self):
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges 0')
|
||||
self.assert_(u'Title: a' in result)
|
||||
@ -343,7 +349,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchangesposid(self):
|
||||
self.b.current_playlist.load([Track(), Track(), Track()])
|
||||
self.b.current_playlist.append([Track(), Track(), Track()])
|
||||
result = self.h.handle_request(u'plchangesposid "0"')
|
||||
self.assert_(u'cpos: 0' in result)
|
||||
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0]
|
||||
@ -357,7 +363,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_without_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -367,7 +373,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_with_open_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -381,7 +387,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_with_closed_range(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -395,7 +401,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_swap(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
@ -409,7 +415,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_swapid(self):
|
||||
self.b.current_playlist.load([
|
||||
self.b.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
|
||||
@ -174,7 +174,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_pause_off(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.h.handle_request(u'play "0"')
|
||||
self.h.handle_request(u'pause "1"')
|
||||
result = self.h.handle_request(u'pause "0"')
|
||||
@ -182,14 +182,14 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_pause_on(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.h.handle_request(u'play "0"')
|
||||
result = self.h.handle_request(u'pause "1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
|
||||
|
||||
def test_pause_toggle(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
@ -201,37 +201,49 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_without_pos(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
result = self.h.handle_request(u'play')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_with_pos(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_with_pos_without_quotes(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play 0')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_with_pos_out_of_bounds(self):
|
||||
self.b.current_playlist.load([])
|
||||
self.b.current_playlist.append([])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
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.append([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.append([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()
|
||||
@ -241,18 +253,30 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.current_track, None)
|
||||
|
||||
def test_playid(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'playid "1"')
|
||||
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.append([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.append([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()
|
||||
@ -262,7 +286,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.current_track, None)
|
||||
|
||||
def test_playid_which_does_not_exist(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'playid "12345"')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
|
||||
|
||||
@ -271,7 +295,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_seek(self):
|
||||
self.b.current_playlist.load([Track(length=40000)])
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.h.handle_request(u'seek "0"')
|
||||
result = self.h.handle_request(u'seek "0" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
@ -279,20 +303,20 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_seek_with_songpos(self):
|
||||
seek_track = Track(uri='2', length=40000)
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(uri='1', length=40000), seek_track])
|
||||
result = self.h.handle_request(u'seek "1" "30"')
|
||||
self.assertEqual(self.b.playback.current_track, seek_track)
|
||||
|
||||
def test_seekid(self):
|
||||
self.b.current_playlist.load([Track(length=40000)])
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
result = self.h.handle_request(u'seekid "1" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(self.b.playback.time_position >= 30000)
|
||||
|
||||
def test_seekid_with_cpid(self):
|
||||
seek_track = Track(uri='2', length=40000)
|
||||
self.b.current_playlist.load(
|
||||
self.b.current_playlist.append(
|
||||
[Track(length=40000), seek_track])
|
||||
result = self.h.handle_request(u'seekid "2" "30"')
|
||||
self.assertEqual(self.b.playback.current_cpid, 2)
|
||||
|
||||
@ -16,7 +16,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_currentsong(self):
|
||||
track = Track()
|
||||
self.b.current_playlist.load([track])
|
||||
self.b.current_playlist.append([track])
|
||||
self.b.playback.play()
|
||||
result = self.h.handle_request(u'currentsong')
|
||||
self.assert_(u'file: ' in result)
|
||||
@ -155,21 +155,21 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(result['state'], 'pause')
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('song' in result)
|
||||
self.assert_(int(result['song']) >= 0)
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('songid' in result)
|
||||
self.assertEqual(int(result['songid']), 1)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||
self.b.current_playlist.load([Track(length=None)])
|
||||
self.b.current_playlist.append([Track(length=None)])
|
||||
self.b.playback.play()
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('time' in result)
|
||||
@ -179,7 +179,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(position <= total)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_length(self):
|
||||
self.b.current_playlist.load([Track(length=10000)])
|
||||
self.b.current_playlist.append([Track(length=10000)])
|
||||
self.b.playback.play()
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('time' in result)
|
||||
@ -196,7 +196,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(int(result['elapsed']), 59123)
|
||||
|
||||
def test_status_method_when_playing_contains_bitrate(self):
|
||||
self.b.current_playlist.load([Track(bitrate=320)])
|
||||
self.b.current_playlist.append([Track(bitrate=320)])
|
||||
self.b.playback.play()
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('bitrate' in result)
|
||||
|
||||
@ -49,12 +49,24 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_load(self):
|
||||
result = self.h.handle_request(u'load "name"')
|
||||
def test_load_known_playlist_appends_to_current_playlist(self):
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 2)
|
||||
self.b.stored_playlists.playlists = [Playlist(name='A-list',
|
||||
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
|
||||
result = self.h.handle_request(u'load "A-list"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a')
|
||||
self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b')
|
||||
self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c')
|
||||
self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd')
|
||||
self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e')
|
||||
|
||||
def test_load_appends(self):
|
||||
raise SkipTest
|
||||
def test_load_unknown_playlist_acks(self):
|
||||
result = self.h.handle_request(u'load "unknown playlist"')
|
||||
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 0)
|
||||
|
||||
def test_playlistadd(self):
|
||||
result = self.h.handle_request(
|
||||
|
||||
@ -27,10 +27,12 @@ class GStreamerOutputTest(unittest.TestCase):
|
||||
def send(self, message):
|
||||
self.output_queue.put(message)
|
||||
|
||||
@SkipTest
|
||||
def test_play_uri_existing_file(self):
|
||||
message = {'command': 'play_uri', 'uri': self.song_uri}
|
||||
self.assertEqual(True, self.send_recv(message))
|
||||
|
||||
@SkipTest
|
||||
def test_play_uri_non_existing_file(self):
|
||||
message = {'command': 'play_uri', 'uri': self.song_uri + 'bogus'}
|
||||
self.assertEqual(False, self.send_recv(message))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user