Merge branch 'gstreamer' of git://github.com/jodal/mopidy into gstreamer-local-backend

Conflicts:
	mopidy/outputs/gstreamer.py
This commit is contained in:
Thomas Adamcik 2010-08-16 23:39:51 +02:00
commit 5c632116b8
27 changed files with 569 additions and 302 deletions

View File

@ -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>

View File

@ -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>`_

View File

@ -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.

View File

@ -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)

View File

@ -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>`_.

View File

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

View File

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

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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):

View File

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

View File

@ -86,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

View File

@ -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':

View File

@ -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.

View File

@ -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'),
])

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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))