diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index fc4b5611..00000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,9 +0,0 @@ -Authors -======= - -Contributors to Mopidy in the order of appearance: - -- Stein Magnus Jodal -- Johannes Knutsen -- Thomas Adamcik -- Kristian Klette diff --git a/README.rst b/README.rst index 350f959b..1e4430e2 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,11 @@ Mopidy ****** -Mopidy is an `Music Player Daemon `_ (MPD) server with a -`Spotify `_ 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 +`_ 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 `_. 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 `_. @@ -14,4 +15,3 @@ To install Mopidy, check out * `Source code `_ * `Issue tracker `_ * IRC: ``#mopidy`` at `irc.freenode.net `_ -* `Presentation of Mopidy `_ diff --git a/docs/authors.rst b/docs/authors.rst index e122f914..f56242a5 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1 +1,22 @@ -.. include:: ../AUTHORS.rst +******* +Authors +******* + +Contributors to Mopidy in the order of appearance: + +- Stein Magnus Jodal +- Johannes Knutsen +- Thomas Adamcik +- Kristian Klette + + +Donations +========= + +If you already enjoy Mopidy, or don't enjoy it and want to help us making +Mopidy better, you can `donate money `_ 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. diff --git a/docs/changes.rst b/docs/changes.rst index 12028a17..323f899e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,58 +22,99 @@ greatly improved MPD client support. `. - 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) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 243243ab..f9588cb8 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -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 `_ recipies - for all our dependencies and Mopidy itself to make OS X installation a - breeze. See `Homebrew's issue #1612 - `_. -- Create `Debian packages `_ 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 `_ + recipies for all our dependencies and Mopidy itself to make OS X + installation a breeze. See `Homebrew's issue #1612 + `_. + - Create `Debian packages `_ 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 `_ frontend, so Mopidy can serve XMMS2 - clients. -- Add support for serving the music as an `Icecast `_ - stream instead of playing it locally. -- Integrate with `Squeezebox `_ in some - way. -- AirPort Express support, like in - `PulseAudio `_. -- DNLA and/or UPnP support. Maybe using - `Coherence `_. -- `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 `_ + - `WIMP `_ + - DNLA/UPnP to Mopidy can play music from other DNLA MediaServers. + +- Frontends: + + - D-Bus/`MPRIS `_ + - REST/JSON web service with a jQuery client as example application. Maybe + based upon `Tornado `_ and `jQuery + Mobile `_. + - DNLA/UPnP to Mopidy can be controlled from i.e. TVs. + - `XMMS2 `_ + - 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 + `_, etc. + - Feed audio to an `Icecast `_ server. + - Stream to AirPort Express using `RAOP + `_. diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 911bf39e..b3ea06fa 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -2,15 +2,21 @@ libspotify installation *********************** -We are working on a -`libspotify `_ backend. -To use the libspotify backend you must install libspotify and -`pyspotify `_. +Mopidy uses `libspotify +`_ for playing music from +the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must +install libspotify and `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 + `_. + +.. 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 diff --git a/docs/licenses.rst b/docs/licenses.rst index c7bf9433..c3a13904 100644 --- a/docs/licenses.rst +++ b/docs/licenses.rst @@ -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 diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index fc17bbee..c8c83a62 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -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.""" diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 2cf15629..f484bf89 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -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 diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 31185cd4..61722c81 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -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) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 7a971bc5..f00ec1f0 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -9,14 +9,18 @@ ENCODING = 'utf-8' class LibspotifyBackend(BaseBackend): """ - A Spotify backend which uses the official `libspotify library - `_. - - `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 `_ backend which uses the official + `libspotify `_ + library and the `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, diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py index 60a5d355..1195e9bc 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/libspotify/playback.py @@ -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: diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py index 3a39aad5..ff8f3c5c 100644 --- a/mopidy/backends/libspotify/translator.py +++ b/mopidy/backends/libspotify/translator.py @@ -39,7 +39,7 @@ class LibspotifyTranslator(object): track_no=spotify_track.index(), date=date, length=spotify_track.duration(), - bitrate=320, + bitrate=160, ) @classmethod diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index c10d1dad..b9111d9e 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -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[^"]*)"( "(?P\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\d+)" "(?P\d+)"$') def swapid(frontend, cpid1, cpid2): diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index bfff275e..7abc4509 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -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): """ diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index ecd8b321..39a2e150 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -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[^"]+)" "(?P[^"]+)"$') 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 diff --git a/mopidy/process.py b/mopidy/process.py index 53b6fbb5..01ac8ed4 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -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': diff --git a/tests/backends/base.py b/tests/backends/base.py index 97719c08..753b093d 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -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) diff --git a/tests/data/blank.flac b/tests/data/blank.flac index b838b98e..ae18d36f 100644 Binary files a/tests/data/blank.flac and b/tests/data/blank.flac differ diff --git a/tests/data/blank.mp3 b/tests/data/blank.mp3 index 3e0b4abb..6aa48cd8 100644 Binary files a/tests/data/blank.mp3 and b/tests/data/blank.mp3 differ diff --git a/tests/data/blank.ogg b/tests/data/blank.ogg index 3b1c57a1..e67e428b 100644 Binary files a/tests/data/blank.ogg and b/tests/data/blank.ogg differ diff --git a/tests/data/blank.wav b/tests/data/blank.wav index 5217ec6f..0041c7ba 100644 Binary files a/tests/data/blank.wav and b/tests/data/blank.wav differ diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 6b5c822e..e27e58c5 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -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'), ]) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index a1331bb3..17263aef 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -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) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 907788f5..9839acfe 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -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) diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index 9babc670..b49ccce1 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -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( diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 1bdd35d9..92e94e01 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -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))