From b6bceacc0f42e32fc3dac4dbe7e17cadaab9542e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 21:23:32 +0100 Subject: [PATCH 01/51] Add Playlist.with() --- mopidy/models.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mopidy/models.py b/mopidy/models.py index 39212c8e..c2e97218 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -217,3 +217,26 @@ class Playlist(object): for track, position in zip(self.tracks, range(start, end)): tracks.append(track.mpd_format(position)) return tracks + + def with(self, uri=None, name=None, tracks=None): + """ + Create a new playlist object with the given values. The values that are + not given are taken from the object the method is called on. + + Does not change the object on which it is called. + + :param uri: playlist URI + :type uri: string + :param name: playlist name + :type name: string + :param tracks: playlist's tracks + :type tracks: list of :class:`Track` elements + :rtype: :class:`Playlist` + """ + if uri is None: + uri = self.uri + if name is None: + name = self.name + if tracks is None: + tracks = self.tracks + return Playlist(uri=uri, name=name, tracks=tracks) From 682b14c13c1034c8b1c4f6a9b547e03e50ed0042 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 21:24:57 +0100 Subject: [PATCH 02/51] Rename to with_() since with is reserved in Python >=2.6 --- mopidy/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/models.py b/mopidy/models.py index c2e97218..37b0853a 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -218,7 +218,7 @@ class Playlist(object): tracks.append(track.mpd_format(position)) return tracks - def with(self, uri=None, name=None, tracks=None): + def with_(self, uri=None, name=None, tracks=None): """ Create a new playlist object with the given values. The values that are not given are taken from the object the method is called on. From 21019740b1892f0e926f861bd20744e9167f4b8c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 22:36:08 +0100 Subject: [PATCH 03/51] Add two new methods for the backend API --- docs/api/backends.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index e20578ea..afcded4d 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -113,6 +113,10 @@ The currently playing or selected :class:`mopidy.models.Track`. + .. method:: new_playlist_loaded_callback() + + Tell the playback controller that a new playlist has been loaded. + .. method:: next() Play the next track. @@ -285,6 +289,13 @@ :param new_name: the new name :type new_name: string + .. method:: save(playlist) + + Save the playlist. + + :param playlist: the playlist + :type playlist: :class:`mopidy.models.Playlist` + .. method:: search(query) Search for playlists whose name contains ``query``. From 969beea69b475e86fc5e0503a6394e51ecf9758e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 22:36:47 +0100 Subject: [PATCH 04/51] Document arguments to Playlist.mpd_format() --- mopidy/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/models.py b/mopidy/models.py index 37b0853a..c1bf83a7 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -209,6 +209,12 @@ class Playlist(object): """ Format playlist for output to MPD client. + Optionally limit output to the slice ``[start:end]`` of the playlist. + + :param start: position of first track to include in output + :type start: int + :param end: position after last track to include in output + :type end: int or :class:`None` for end of list :rtype: list of lists of two-tuples """ if end is None: From 035e43b4f5430e6cf8201ae038f46fe07361350e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 22:56:22 +0100 Subject: [PATCH 05/51] Replace BaseBackend with new API, keeping the existing logic. MPD formatting moved to MpdHandler. --- mopidy/backends/__init__.py | 314 ++++++++++++++++++------------------ mopidy/mpd/handler.py | 39 ++++- 2 files changed, 191 insertions(+), 162 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index af3e1b23..2cb4c692 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -1,23 +1,96 @@ +from copy import copy import logging +import random import time -from mopidy.exceptions import MpdNotImplemented from mopidy.models import Playlist logger = logging.getLogger('backends.base') class BaseBackend(object): - PLAY = u'play' - PAUSE = u'pause' - STOP = u'stop' + current_playlist = None + library = None + playback = None + stored_playlists = None + uri_handlers = [] - def __init__(self, *args, **kwargs): - self._state = self.STOP - self._playlists = [] - self._x_current_playlist = Playlist() - self._current_playlist_version = 0 -# Backend state +class BaseCurrentPlaylistController(object): + def __init__(self, backend): + self.backend = backend + self.version = 0 + self.playlist = Playlist() + + @property + def playlist(self): + return copy(self._playlist) + + @playlist.setter + def playlist(self, new_playlist): + self._playlist = new_playlist + self.version += 1 + + def add(self, uri, at_position=None): + raise NotImplementedError + + def clear(self): + self.backend.playback.stop() + self.playlist = Playlist() + + def load(self, playlist): + self.playlist = playlist + self.version = 0 + + def move(self, start, end, to_position): + tracks = self.playlist.tracks + new_tracks = tracks[:start] + tracks[end:] + + for track in tracks[start:end]: + new_tracks.insert(to_position, track) + to_position += 1 + + self.playlist = self.playlist.with_(tracks=new_tracks) + + def remove(self, position): + tracks = self.playlist.tracks + del tracks[position] + self.playlist = self.playlist.with_(tracks=tracks) + + def shuffle(self, start=None, end=None): + tracks = self.playlist.tracks + + before = tracks[:start or 0] + shuffled = tracks[start:end] + after = tracks[end or len(tracks):] + + random.shuffle(shuffled) + + self.playlist = self.playlist.with_(tracks=before+shuffled+after) + + +class BasePlaybackController(object): + PAUSED = u'paused' + PLAYING = u'playing' + STOPPED = u'stopped' + + def __init__(self, backend): + self.backend = backend + self.consume = False + self.current_track = None + self.random = False + self.repeat = False + self.state = self.STOPPED + self.volume = None + + @property + def playlist_position(self): + if self.current_track is None: + return None + try: + return self.backend.current_playlist.playlist.index( + self.current_track) + except ValueError: + return None @property def state(self): @@ -35,7 +108,7 @@ class BaseBackend(object): self._play_time_resume() @property - def _play_time_elapsed(self): + def time_position(self): if self.state == self.PLAY: time_since_started = int(time.time()) - self._play_time_started return self._play_time_accumulated + time_since_started @@ -55,184 +128,113 @@ class BaseBackend(object): def _play_time_resume(self): self._play_time_started = int(time.time()) - @property - def _current_playlist(self): - return self._x_current_playlist - - @_current_playlist.setter - def _current_playlist(self, playlist): - self._x_current_playlist = playlist - self._current_playlist_version += 1 - - @property - def _current_track(self): - if self._current_song_pos is not None: - return self._current_playlist.tracks[self._current_song_pos] - - @property - def _current_song_pos(self): - if not hasattr(self, '_x_current_song_pos'): - self._x_current_song_pos = None - if (self._current_playlist is None - or self._current_playlist.length == 0): - self._x_current_song_pos = None - elif self._x_current_song_pos < 0: - self._x_current_song_pos = 0 - elif self._x_current_song_pos >= self._current_playlist.length: - self._x_current_song_pos = self._current_playlist.length - 1 - return self._x_current_song_pos - - @_current_song_pos.setter - def _current_song_pos(self, songid): - self._x_current_song_pos = songid - -# Status methods - - def current_song(self): - if self.state is not self.STOP and self._current_track is not None: - return self._current_track.mpd_format(self._current_song_pos) - - def status_bitrate(self): - return 0 - - def status_consume(self): - return 0 - - def status_volume(self): - return 0 - - def status_repeat(self): - return 0 - - def status_random(self): - return 0 - - def status_single(self): - return 0 - - def status_song_id(self): - return self._current_song_pos # Override if you got a better ID scheme - - def status_playlist(self): - return self._current_playlist_version - - def status_playlist_length(self): - return self._current_playlist.length - - def status_state(self): - return self.state - - def status_time(self): - return u'%s:%s' % (self._play_time_elapsed, self.status_time_total()) - - def status_time_total(self): - if self._current_track is not None: - return self._current_track.length // 1000 - else: - return 0 - - def status_xfade(self): - return 0 - - def url_handlers(self): - return [] - -# Control methods - - def end_of_track(self): - self.next() + def new_playlist_loaded_callback(self): + self.current_track = None + if self.state == self.PLAYING: + if self.backend.current_playlist.playlist.length > 0: + self.play(self.backend.current_playlist.playlist.tracks[0]) + else: + self.stop() def next(self): self.stop() if self._next(): - self.state = self.PLAY + self.state = self.PLAYING def _next(self): - raise MpdNotImplemented + raise NotImplementedError def pause(self): - if self.state == self.PLAY and self._pause(): - self.state = self.PAUSE + if self.state == self.PLAYING and self._pause(): + self.state = self.PAUSED def _pause(self): - raise MpdNotImplemented + raise NotImplementedError - def play(self, songpos=None, songid=None): - if self.state == self.PAUSE and songpos is None and songid is None: + def play(self, track=None): + if self.state == self.PAUSED and track is None: return self.resume() + if track is not None: + self.current_track = track self.stop() - if songpos is not None and self._play_pos(songpos): - self.state = self.PLAY - elif songid is not None and self._play_id(songid): - self.state = self.PLAY - elif self._play(): - self.state = self.PLAY + if self._play(track): + self.state = self.PLAYING - def _play(self): - raise MpdNotImplemented - - def _play_id(self, songid): - raise MpdNotImplemented - - def _play_pos(self, songpos): - raise MpdNotImplemented + def _play(self, track): + raise NotImplementedError def previous(self): self.stop() if self._previous(): - self.state = self.PLAY + self.state = self.PLAYING def _previous(self): - raise MpdNotImplemented + raise NotImplementedError def resume(self): - if self.state == self.PAUSE and self._resume(): - self.state = self.PLAY + if self.state == self.PAUSED and self._resume(): + self.state = self.PLAYING def _resume(self): - raise MpdNotImplemented + raise NotImplementedError + + def seek(self, time_position): + raise NotImplementedError def stop(self): - if self.state != self.STOP and self._stop(): - self.state = self.STOP + if self.state != self.STOPPED and self._stop(): + self.state = self.STOPPED def _stop(self): - raise MpdNotImplemented + raise NotImplementedError -# Current/single playlist methods - def playlist_load(self, name): - self._current_song_pos = None - matches = filter(lambda p: p.name == name, self._playlists) - if matches: - self._current_playlist = matches[0] - if self.state == self.PLAY: - self.play(songpos=0) - else: - self._current_playlist = None +class BaseLibraryController(object): + def __init__(self, backend): + self.backend = backend - def playlist_changes_since(self, version='0'): - if int(version) < self._current_playlist_version: - return self._current_playlist.mpd_format() + def find_exact(self, type, query): + raise NotImplementedError - def playlist_info(self, songpos=None, start=0, end=None): - if songpos is not None: - start = int(songpos) - end = start + 1 - else: - if start is None: - start = 0 - start = int(start) - if end is not None: - end = int(end) - return self._current_playlist.mpd_format(start, end) + def lookup(self, uri): + raise NotImplementedError -# Stored playlist methods + def refresh(self, uri=None): + raise NotImplementedError - def playlists_list(self): - return [u'playlist: %s' % p.name for p in self._playlists] + def search(self, type, query): + raise NotImplementedError -# Music database methods - def search(self, type, what): - return None +class BaseStoredPlaylistController(object): + def __init__(self, backend): + self.backend = backend + self._playlists = [] + + @property + def playlists(self): + return copy(self._playlists) + + def add(self, uri): + raise NotImplementedError + + def create(self, name): + raise NotImplementedError + + def delete(self, playlist): + raise NotImplementedError + + def lookup(self, uri): + raise NotImplementedError + + def refresh(self): + raise NotImplementedError + + def rename(self, playlist, new_name): + raise NotImplementedError + + def save(self, playlist): + raise NotImplementedError + + def search(self, query): + return filter(lambda p: query in p.name, self._playlists) diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index a331d2bd..a41d0b15 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -189,11 +189,15 @@ class MpdHandler(object): @register(r'^listplaylists$') def _listplaylists(self): - return self.backend.playlists_list() + return [u'playlist: %s' % p.name + for p in self.backend.stored_playlists.playlists] @register(r'^load "(?P[^"]+)"$') def _load(self, name): - return self.backend.playlist_load(name) + matches = self.backend.stored_playlists.search(name) + if matches: + self.backend.current_playlist.load(matches[0]) + self.backend.playback.new_playlist_loaded_callback() @register(r'^lsinfo$') @register(r'^lsinfo "(?P[^"]*)"$') @@ -264,13 +268,22 @@ class MpdHandler(object): @register(r'^playlistid( "(?P\S+)")*$') def _playlistid(self, songid=None): - return self.backend.playlist_info(songid, None, None) + return self.backend.current_playlist.playlist.mpd_format() @register(r'^playlistinfo$') @register(r'^playlistinfo "(?P\d+)"$') @register(r'^playlistinfo "(?P\d+):(?P\d+)*"$') def _playlistinfo(self, songpos=None, start=None, end=None): - return self.backend.playlist_info(songpos, start, end) + if songpos is not None: + return self.backend.current_playlist.playlist.mpd_format( + songpos, songpos + 1) + else: + if start is None: + start = 0 + start = int(start) + if end is not None: + end = int(end) + return self.backend.current_playlist.playlist.mpd_format(start, end) @register(r'^playlistmove "(?P[^"]+)" "(?P\d+)" "(?P\d+)"$') def _playlistdelete(self, name, songid, songpos): @@ -282,7 +295,8 @@ class MpdHandler(object): @register(r'^plchanges "(?P\d+)"$') def _plchanges(self, version): - return self.backend.playlist_changes_since(version) + if int(version) < self.backend.current_playlist.version: + return self.backend.current_playlist.playlist.mpd_format() @register(r'^plchangesposid "(?P\d+)"$') def _plchangesposid(self, version): @@ -400,10 +414,23 @@ class MpdHandler(object): result.append(('song', self.backend.status_song_id())) result.append(('songid', self.backend.status_song_id())) if self.backend.state in (self.backend.PLAY, self.backend.PAUSE): - result.append(('time', self.backend.status_time())) + result.append(('time', self._status_time())) result.append(('bitrate', self.backend.status_bitrate())) return result + def _status_time(self): + return u'%s:%s' % ( + self._status_time_elapsed(), self._status_time_total()) + + def _status_time_elapsed(self): + return self.backend.playback.time_position + + def _status_time_total(self): + if self.backend.playback.current_track is not None: + return self.backend.playback.current_track.length // 1000 + else: + return 0 + @register(r'^swap "(?P\d+)" "(?P\d+)"$') def _swap(self, songpos1, songpos2): raise MpdNotImplemented # TODO From 0eaee6b70d9d038a5a900cc3462fa95b8667e4ae Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 23:10:25 +0100 Subject: [PATCH 06/51] Reorder classes --- mopidy/backends/__init__.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 2cb4c692..41cc4289 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -68,6 +68,23 @@ class BaseCurrentPlaylistController(object): self.playlist = self.playlist.with_(tracks=before+shuffled+after) +class BaseLibraryController(object): + def __init__(self, backend): + self.backend = backend + + def find_exact(self, type, query): + raise NotImplementedError + + def lookup(self, uri): + raise NotImplementedError + + def refresh(self, uri=None): + raise NotImplementedError + + def search(self, type, query): + raise NotImplementedError + + class BasePlaybackController(object): PAUSED = u'paused' PLAYING = u'playing' @@ -189,23 +206,6 @@ class BasePlaybackController(object): raise NotImplementedError -class BaseLibraryController(object): - def __init__(self, backend): - self.backend = backend - - def find_exact(self, type, query): - raise NotImplementedError - - def lookup(self, uri): - raise NotImplementedError - - def refresh(self, uri=None): - raise NotImplementedError - - def search(self, type, query): - raise NotImplementedError - - class BaseStoredPlaylistController(object): def __init__(self, backend): self.backend = backend From 3d7fc78010389f6b3a35a8b242f2c038dfd4e10b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 23:16:40 +0100 Subject: [PATCH 07/51] Fix old use of state constants --- mopidy/backends/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 41cc4289..f453a83b 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -92,6 +92,7 @@ class BasePlaybackController(object): def __init__(self, backend): self.backend = backend + self._state = self.STOPPED self.consume = False self.current_track = None self.random = False @@ -117,21 +118,22 @@ class BasePlaybackController(object): def state(self, new_state): (old_state, self._state) = (self.state, new_state) logger.debug(u'Changing state: %s -> %s', old_state, new_state) - if old_state in (self.PLAY, self.STOP) and new_state == self.PLAY: + if (old_state in (self.PLAYING, self.STOPPED) + and new_state == self.PLAYING): self._play_time_start() - elif old_state == self.PLAY and new_state == self.PAUSE: + elif old_state == self.PLAYING and new_state == self.PAUSED: self._play_time_pause() - elif old_state == self.PAUSE and new_state == self.PLAY: + elif old_state == self.PAUSED and new_state == self.PLAYING: self._play_time_resume() @property def time_position(self): - if self.state == self.PLAY: + if self.state == self.PLAYING: time_since_started = int(time.time()) - self._play_time_started return self._play_time_accumulated + time_since_started - elif self.state == self.PAUSE: + elif self.state == self.PAUSED: return self._play_time_accumulated - elif self.state == self.STOP: + elif self.state == self.STOPPED: return 0 def _play_time_start(self): From 2a30d40ed518e1bf54c58533a5fd25b1b9ac969f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 23:17:22 +0100 Subject: [PATCH 08/51] Fix typo in BaseStoredPlaylistsController class name --- mopidy/backends/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index f453a83b..99e8b2d6 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -208,7 +208,7 @@ class BasePlaybackController(object): raise NotImplementedError -class BaseStoredPlaylistController(object): +class BaseStoredPlaylistsController(object): def __init__(self, backend): self.backend = backend self._playlists = [] From acdfff5b61643ae62d5df2e141a7e6d6266bbd6f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Feb 2010 23:36:53 +0100 Subject: [PATCH 09/51] Add get_by_id() and get_by_uri() to BaseCurrentPlaylistController --- docs/api/backends.rst | 14 ++++++++++++++ mopidy/backends/__init__.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index afcded4d..fe5a96e7 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -52,6 +52,20 @@ Clear the current playlist. + .. method:: get_by_id(id) + + Get track by ID. Raises :class:`KeyError` if not found. + + :param id: track ID + :type id: int + + .. method:: get_by_uri(uri) + + Get track by URI. Raises :class:`KeyError` if not found. + + :param uri: track URI + :type uri: string + .. method:: load(playlist) Replace the current playlist with the given playlist. diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 99e8b2d6..416a64a0 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -37,6 +37,20 @@ class BaseCurrentPlaylistController(object): self.backend.playback.stop() self.playlist = Playlist() + def get_by_id(self, id): + matches = filter(lambda t: t.id == id, self._playlist.tracks) + if matches: + return matches[0] + else: + raise KeyError('Track with ID "%s" not found' % id) + + def get_by_uri(self, uri): + matches = filter(lambda t: t.uri == uri, self._playlist.tracks) + if matches: + return matches[0] + else: + raise KeyError('Track with URI "%s" not found' % uri) + def load(self, playlist): self.playlist = playlist self.version = 0 From e981edc2ccbf2731774137bfba0fb465f58c86a3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 00:10:50 +0100 Subject: [PATCH 10/51] Fix error in BasePlaybackController.playlist_position() --- mopidy/backends/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 416a64a0..6f1ddd69 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -119,7 +119,7 @@ class BasePlaybackController(object): if self.current_track is None: return None try: - return self.backend.current_playlist.playlist.index( + return self.backend.current_playlist.playlist.tracks.index( self.current_track) except ValueError: return None From 98e333bafafc0161a256b2df895d269825910aab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 00:11:36 +0100 Subject: [PATCH 11/51] Update DummyBackend to adhere to new backend API --- mopidy/backends/dummy.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index 8ebf3ac2..d88e3a0b 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -1,25 +1,30 @@ -from mopidy.backends import BaseBackend +from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BasePlaybackController, BaseLibraryController, + BaseStoredPlaylistsController) class DummyBackend(BaseBackend): - def __init__(self, *args, **kwargs): - super(DummyBackend, self).__init__(*args, **kwargs) + def __init__(self): + self.current_playlist = DummyCurrentPlaylistController(backend=self) + self.library = DummyLibraryController(backend=self) + self.playback = DummyPlaybackController(backend=self) + self.stored_playlists = DummyStoredPlaylistsController(backend=self) + self.uri_handlers = [u'dummy:'] - def url_handlers(self): - return [u'dummy:'] +class DummyCurrentPlaylistController(BaseCurrentPlaylistController): + pass +class DummyLibraryController(BaseLibraryController): + def search(self, type, query): + return [] + +class DummyPlaybackController(BasePlaybackController): def _next(self): return True def _pause(self): return True - def _play(self): - return True - - def _play_id(self, songid): - return True - - def _play_pos(self, songpos): + def _play(self, track): return True def _previous(self): @@ -27,3 +32,6 @@ class DummyBackend(BaseBackend): def _resume(self): return True + +class DummyStoredPlaylistsController(BaseStoredPlaylistsController): + pass From 46af63ab7ecb5788ec9aae7019ae74a6ac4b68f1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 00:12:05 +0100 Subject: [PATCH 12/51] Update MpdHandler to use new backend API --- mopidy/mpd/handler.py | 120 +++++++++++++++++++++++++++++++-------- tests/mpd/handlertest.py | 26 ++++++--- 2 files changed, 113 insertions(+), 33 deletions(-) diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index a41d0b15..41ea21ce 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -132,7 +132,8 @@ class MpdHandler(object): @register(r'^currentsong$') def _currentsong(self): - return self.backend.current_song() + if self.backend.playback.current_track is not None: + return self.backend.playback.current_track.mpd_format() @register(r'^delete "(?P\d+)"$') @register(r'^delete "(?P\d+):(?P\d+)*"$') @@ -217,7 +218,7 @@ class MpdHandler(object): @register(r'^next$') def _next(self): - return self.backend.next() + return self.backend.playback.next() @register(r'^password "(?P[^"]+)"$') def _password(self, password): @@ -226,9 +227,9 @@ class MpdHandler(object): @register(r'^pause "(?P[01])"$') def _pause(self, state): if int(state): - self.backend.pause() + self.backend.playback.pause() else: - self.backend.resume() + self.backend.playback.resume() @register(r'^ping$') def _ping(self): @@ -236,15 +237,25 @@ class MpdHandler(object): @register(r'^play$') def _play(self): - return self.backend.play() + return self.backend.playback.play() @register(r'^play "(?P\d+)"$') def _playpos(self, songpos): - return self.backend.play(songpos=int(songpos)) + songpos = int(songpos) + try: + track = self.backend.current_playlist.playlist.tracks[songpos] + return self.backend.playback.play(track) + except IndexError: + raise MpdAckError(u'Position out of bounds') @register(r'^playid "(?P\d+)"$') def _playid(self, songid): - return self.backend.play(songid=int(songid)) + songid = int(songid) + try: + track = self.backend.current_playlist.get_by_id(songid) + return self.backend.playback.play(track) + except KeyError, e: + raise MpdAckError(unicode(e)) @register(r'^playlist$') def _playlist(self): @@ -275,6 +286,7 @@ class MpdHandler(object): @register(r'^playlistinfo "(?P\d+):(?P\d+)*"$') def _playlistinfo(self, songpos=None, start=None, end=None): if songpos is not None: + songpos = int(songpos) return self.backend.current_playlist.playlist.mpd_format( songpos, songpos + 1) else: @@ -304,7 +316,7 @@ class MpdHandler(object): @register(r'^previous$') def _previous(self): - return self.backend.previous() + return self.backend.playback.previous() @register(r'^rename "(?P[^"]+)" "(?P[^"]+)"$') def _rename(self, old_name, new_name): @@ -348,7 +360,7 @@ class MpdHandler(object): @register(r'^search "(?P(album|artist|filename|title))" "(?P[^"]+)"$') def _search(self, type, what): - return self.backend.search(type, what) + return self.backend.library.search(type, what) @register(r'^seek "(?P\d+)" "(?P\d+)"$') def _seek(self, songpos, seconds): @@ -395,29 +407,78 @@ class MpdHandler(object): @register(r'^stop$') def _stop(self): - self.backend.stop() + self.backend.playback.stop() @register(r'^status$') def _status(self): result = [ - ('volume', self.backend.status_volume()), - ('repeat', self.backend.status_repeat()), - ('random', self.backend.status_random()), - ('single', self.backend.status_single()), - ('consume', self.backend.status_consume()), - ('playlist', self.backend.status_playlist()), - ('playlistlength', self.backend.status_playlist_length()), - ('xfade', self.backend.status_xfade()), - ('state', self.backend.status_state()), + ('volume', self._status_volume()), + ('repeat', self._status_repeat()), + ('random', self._status_random()), + ('single', self._status_single()), + ('consume', self._status_consume()), + ('playlist', self._status_playlist_version()), + ('playlistlength', self._status_playlist_length()), + ('xfade', self._status_xfade()), + ('state', self._status_state()), ] - if self.backend.status_playlist_length() > 0: - result.append(('song', self.backend.status_song_id())) - result.append(('songid', self.backend.status_song_id())) - if self.backend.state in (self.backend.PLAY, self.backend.PAUSE): + if self.backend.playback.current_track is not None: + result.append(('song', self._status_songpos())) + result.append(('songid', self._status_songid())) + if self.backend.playback.state in ( + self.backend.playback.PLAYING, self.backend.playback.PAUSED): result.append(('time', self._status_time())) - result.append(('bitrate', self.backend.status_bitrate())) + result.append(('bitrate', self._status_bitrate())) return result + def _status_bitrate(self): + if self.backend.playback.current_track is not None: + return self.backend.playback.current_track.bitrate + + def _status_consume(self): + if self.backend.playback.consume: + return 1 + else: + return 0 + + def _status_playlist_length(self): + return self.backend.current_playlist.playlist.length + + def _status_playlist_version(self): + return self.backend.current_playlist.version + + def _status_random(self): + if self.backend.playback.random: + return 1 + else: + return 0 + + def _status_repeat(self): + if self.backend.playback.repeat: + return 1 + else: + return 0 + + def _status_single(self): + return 0 # TODO + + def _status_songid(self): + if self.backend.playback.current_track.id is not None: + return self.backend.playback.current_track.id + else: + return self._status_songpos() + + def _status_songpos(self): + return self.backend.playback.playlist_position + + def _status_state(self): + if self.backend.playback.state == self.backend.playback.PLAYING: + return u'play' + elif self.backend.playback.state == self.backend.playback.STOPPED: + return u'stop' + elif self.backend.playback.state == self.backend.playback.PAUSED: + return u'pause' + def _status_time(self): return u'%s:%s' % ( self._status_time_elapsed(), self._status_time_total()) @@ -431,6 +492,15 @@ class MpdHandler(object): else: return 0 + def _status_volume(self): + if self.backend.playback.volume is not None: + return self.backend.playback.volume + else: + return 0 + + def _status_xfade(self): + return 0 # TODO + @register(r'^swap "(?P\d+)" "(?P\d+)"$') def _swap(self, songpos1, songpos2): raise MpdNotImplemented # TODO @@ -445,4 +515,4 @@ class MpdHandler(object): @register(r'^urlhandlers$') def _urlhandlers(self): - return self.backend.url_handlers() + return self.backend.uri_handlers diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index 653044db..70304a36 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -152,7 +152,9 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(result['state'] in ('play', 'stop', 'pause')) def test_status_method_when_playlist_loaded(self): - self.b._current_playlist = Playlist(tracks=[Track()]) + track = Track() + self.b.current_playlist.load(Playlist(tracks=[track])) + self.b.playback.current_track = track result = dict(self.h._status()) self.assert_('song' in result) self.assert_(int(result['song']) >= 0) @@ -160,7 +162,7 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(int(result['songid']) >= 0) def test_status_method_when_playing(self): - self.b.state = self.b.PLAY + self.b.playback.state = self.b.playback.PLAYING result = dict(self.h._status()) self.assert_('time' in result) (position, total) = result['time'].split(':') @@ -283,28 +285,36 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') self.assert_(u'OK' in result) - self.assertEquals(self.b.PLAY, self.b.state) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) def test_pause_on(self): self.h.handle_request(u'play') result = self.h.handle_request(u'pause "1"') self.assert_(u'OK' in result) - self.assertEquals(self.b.PAUSE, self.b.state) + self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) def test_play_without_pos(self): result = self.h.handle_request(u'play') self.assert_(u'OK' in result) - self.assertEquals(self.b.PLAY, self.b.state) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) def test_play_with_pos(self): + self.b.current_playlist.load(Playlist(tracks=[Track()])) result = self.h.handle_request(u'play "0"') self.assert_(u'OK' in result) - self.assertEquals(self.b.PLAY, self.b.state) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + + def test_play_with_pos_out_of_bounds(self): + self.b.current_playlist.load(Playlist()) + result = self.h.handle_request(u'play "0"') + self.assert_(u'ACK Position out of bounds' in result) + self.assertEquals(self.b.playback.STOPPED, self.b.playback.state) def test_playid(self): + self.b.current_playlist.load(Playlist(tracks=[Track(id=0)])) result = self.h.handle_request(u'playid "0"') self.assert_(u'OK' in result) - self.assertEquals(self.b.PLAY, self.b.state) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) def test_previous(self): result = self.h.handle_request(u'previous') @@ -321,7 +331,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): def test_stop(self): result = self.h.handle_request(u'stop') self.assert_(u'OK' in result) - self.assertEquals(self.b.STOP, self.b.state) + self.assertEquals(self.b.playback.STOPPED, self.b.playback.state) class CurrentPlaylistHandlerTest(unittest.TestCase): From 13819b62b3e72446977f54160c414eef55150ca7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 00:20:25 +0100 Subject: [PATCH 13/51] Test full currentsong output --- mopidy/models.py | 15 ++++++++------- tests/mpd/handlertest.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index c1bf83a7..8880651e 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -150,15 +150,16 @@ class Track(object): :rtype: list of two-tuples """ return [ - ('file', self.uri), - ('Time', self.length // 1000), + ('file', self.uri or ''), + ('Time', self.length and (self.length // 1000) or 0), ('Artist', self.mpd_format_artists()), - ('Title', self.title), - ('Album', self.album.name), - ('Track', '%d/%d' % (self.track_no, self.album.num_tracks)), - ('Date', self.date), + ('Title', self.title or ''), + ('Album', self.album and self.album.name or ''), + ('Track', '%d/%d' % (self.track_no, + self.album and self.album.num_tracks or 0)), + ('Date', self.date or ''), ('Pos', position), - ('Id', self.id), + ('Id', self.id or position), ] def mpd_format_artists(self): diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index 70304a36..c130725d 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -94,6 +94,20 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'ACK Not implemented' in result) def test_currentsong(self): + self.b.playback.current_track = Track() + result = self.h.handle_request(u'currentsong') + self.assert_(u'file: ' in result) + self.assert_(u'Time: 0' in result) + self.assert_(u'Artist: ' in result) + self.assert_(u'Title: ' in result) + self.assert_(u'Album: ' in result) + self.assert_(u'Track: 0/0' in result) + self.assert_(u'Date: ' in result) + self.assert_(u'Pos: 0' in result) + self.assert_(u'Id: 0' in result) + self.assert_(u'OK' in result) + + def test_currentsong_without_song(self): result = self.h.handle_request(u'currentsong') self.assert_(u'OK' in result) From 63eba83dd0cec8346dae9dca74c54cb297aef4ca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 09:39:36 +0100 Subject: [PATCH 14/51] Add Thomas Adamcik to AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 8e08456e..65e7d950 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,3 +5,4 @@ Contributors to Mopidy in the order of appearance: * Stein Magnus Jodal * Johannes Knutsen +* Thomas Adamcik From c4cb47df6c390c85f9bf0c4129aa3a585a522a30 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 12:11:34 +0100 Subject: [PATCH 15/51] Move Backend API docs from docs/ to docstrings --- docs/api/backends.rst | 317 +----------------------------------- mopidy/backends/__init__.py | 237 +++++++++++++++++++++++++-- 2 files changed, 224 insertions(+), 330 deletions(-) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index fe5a96e7..56d6f059 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -2,318 +2,7 @@ :mod:`mopidy.backends` -- Backend API ************************************* -.. warning:: - This is our *planned* backend API, and not the current API. - -.. module:: mopidy.backends +.. automodule:: mopidy.backends :synopsis: Interface between Mopidy and its various backends. - -.. class:: BaseBackend() - - .. attribute:: current_playlist - - The current playlist controller. An instance of - :class:`BaseCurrentPlaylistController`. - - .. attribute:: library - - The library controller. An instance of :class:`BaseLibraryController`. - - .. attribute:: playback - - The playback controller. An instance of :class:`BasePlaybackController`. - - .. attribute:: stored_playlists - - The stored playlists controller. An instance of - :class:`BaseStoredPlaylistsController`. - - .. attribute:: uri_handlers - - List of URI prefixes this backend can handle. - - -.. class:: BaseCurrentPlaylistController(backend) - - :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` - - .. method:: add(track, at_position=None) - - Add the track to the end of, or at the given position in the current - playlist. - - :param track: track to add - :type track: :class:`mopidy.models.Track` - :param at_position: position in current playlist to add track - :type at_position: int or :class:`None` - - .. method:: clear() - - Clear the current playlist. - - .. method:: get_by_id(id) - - Get track by ID. Raises :class:`KeyError` if not found. - - :param id: track ID - :type id: int - - .. method:: get_by_uri(uri) - - Get track by URI. Raises :class:`KeyError` if not found. - - :param uri: track URI - :type uri: string - - .. method:: load(playlist) - - Replace the current playlist with the given playlist. - - :param playlist: playlist to load - :type playlist: :class:`mopidy.models.Playlist` - - .. method:: move(start, end, to_position) - - Move the tracks in the slice ``[start:end]`` to ``to_position``. - - :param start: position of first track to move - :type start: int - :param end: position after last track to move - :type end: int - :param to_position: new position for the tracks - :type to_position: int - - .. attribute:: playlist - - The currently loaded :class:`mopidy.models.Playlist`. - - .. method:: remove(track) - - Remove the track from the current playlist. - - :param track: track to remove - :type track: :class:`mopidy.models.Track` - - .. method:: shuffle(start=None, end=None) - - Shuffles the entire playlist. If ``start`` and ``end`` is given only - shuffles the slice ``[start:end]``. - - :param start: position of first track to shuffle - :type start: int or :class:`None` - :param end: position after last track to shuffle - :type end: int or :class:`None` - - .. attribute:: version - - The current playlist version. Integer which is increased every time the - current playlist is changed. - - -.. class:: BasePlaybackController(backend) - - :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` - - .. attribute:: consume - - :class:`True` - Tracks are removed from the playlist when they have been played. - :class:`False` - Tracks are not removed from the playlist. - - .. attribute:: current_track - - The currently playing or selected :class:`mopidy.models.Track`. - - .. method:: new_playlist_loaded_callback() - - Tell the playback controller that a new playlist has been loaded. - - .. method:: next() - - Play the next track. - - .. method:: pause() - - Pause playblack. - - .. attribute:: PAUSED - - Constant representing the paused state. - - .. method:: play(track=None) - - Play the given track or the currently active track. - - :param track: track to play - :type track: :class:`mopidy.models.Track` or :class:`None` - - .. attribute:: PLAYING - - Constant representing the playing state. - - .. attribute:: playlist_position - - The position in the current playlist. - - .. method:: previous() - - Play the previous track. - - .. attribute:: random - - :class:`True` - Tracks are selected at random from the playlist. - :class:`False` - Tracks are played in the order of the playlist. - - .. attribute:: repeat - - :class:`True` - The current track is played repeatedly. - :class:`False` - The current track is played once. - - .. method:: resume() - - If paused, resume playing the current track. - - .. method:: seek(time_position) - - Seeks to time position given in milliseconds. - - :param time_position: time position in milliseconds - :type time_position: int - - .. attribute:: state - - The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or - :attr:`STOPPED`. - - .. method:: stop() - - Stop playing. - - .. attribute:: STOPPED - - Constant representing the stopped state. - - .. attribute:: time_position - - Time position in milliseconds. - - .. attribute:: volume - - The audio volume as an int in the range [0, 100]. :class:`None` if - unknown. - - -.. class:: BaseLibraryController(backend) - - :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` - - .. method:: find_exact(type, query) - - Find tracks in the library where ``type`` matches ``query`` exactly. - - :param type: 'title', 'artist', or 'album' - :type type: string - :param query: the search query - :type query: string - :rtype: list of :class:`mopidy.models.Track` - - .. method:: lookup(uri) - - Lookup track with given URI. - - :param uri: track URI - :type uri: string - :rtype: :class:`mopidy.models.Track` - - .. method:: refresh(uri=None) - - Refresh library. Limit to URI and below if an URI is given. - - :param uri: directory or track URI - :type uri: string - - .. method:: search(type, query) - - Search the library for tracks where ``type`` contains ``query``. - - :param type: 'title', 'artist', 'album', or 'uri' - :type type: string - :param query: the search query - :type query: string - :rtype: list of :class:`mopidy.models.Track` - - -.. class:: BaseStoredPlaylistsController(backend) - - :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` - - .. method:: add(uri) - - Add existing playlist with the given URI. - - :param uri: URI of existing playlist - :type uri: string - - .. method:: create(name) - - Create a new playlist. - - :param name: name of the new playlist - :type name: string - :rtype: :class:`mopidy.models.Playlist` - - .. attribute:: playlists - - List of :class:`mopidy.models.Playlist`. - - .. method:: delete(playlist) - - Delete playlist. - - :param playlist: the playlist to delete - :type playlist: :class:`mopidy.models.Playlist` - - .. method:: lookup(uri) - - Lookup playlist with given URI. - - :param uri: playlist URI - :type uri: string - :rtype: :class:`mopidy.models.Playlist` - - .. method:: refresh() - - Refresh stored playlists. - - .. method:: rename(playlist, new_name) - - Rename playlist. - - :param playlist: the playlist - :type playlist: :class:`mopidy.models.Playlist` - :param new_name: the new name - :type new_name: string - - .. method:: save(playlist) - - Save the playlist. - - :param playlist: the playlist - :type playlist: :class:`mopidy.models.Playlist` - - .. method:: search(query) - - Search for playlists whose name contains ``query``. - - :param query: query to search for - :type query: string - :rtype: list of :class:`mopidy.models.Playlist` + :members: + :undoc-members: diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 6f1ddd69..92e79000 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -8,21 +8,41 @@ from mopidy.models import Playlist logger = logging.getLogger('backends.base') class BaseBackend(object): + #: The current playlist controller. An instance of + #: :class:`BaseCurrentPlaylistController`. current_playlist = None + + #: The library controller. An instance of :class:`BaseLibraryController`. library = None + + #: The playback controller. An instance of :class:`BasePlaybackController`. playback = None + + #: The stored playlists controller. An instance of + #: :class:`BaseStoredPlaylistsController`. stored_playlists = None + + #: List of URI prefixes this backend can handle. uri_handlers = [] class BaseCurrentPlaylistController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + + #: The current playlist version. Integer which is increased every time the + #: current playlist is changed. + version = 0 + def __init__(self, backend): self.backend = backend - self.version = 0 self.playlist = Playlist() @property def playlist(self): + """The currently loaded :class:`mopidy.models.Playlist`.""" return copy(self._playlist) @playlist.setter @@ -30,14 +50,30 @@ class BaseCurrentPlaylistController(object): self._playlist = new_playlist self.version += 1 - def add(self, uri, at_position=None): + def add(self, track, at_position=None): + """ + Add the track to the end of, or at the given position in the current + playlist. + + :param track: track to add + :type track: :class:`mopidy.models.Track` + :param at_position: position in current playlist to add track + :type at_position: int or :class:`None` + """ raise NotImplementedError def clear(self): + """Clear the current playlist.""" self.backend.playback.stop() self.playlist = Playlist() def get_by_id(self, id): + """ + Get track by ID. Raises :class:`KeyError` if not found. + + :param id: track ID + :type id: int + """ matches = filter(lambda t: t.id == id, self._playlist.tracks) if matches: return matches[0] @@ -45,6 +81,12 @@ class BaseCurrentPlaylistController(object): raise KeyError('Track with ID "%s" not found' % id) def get_by_uri(self, uri): + """ + Get track by URI. Raises :class:`KeyError` if not found. + + :param uri: track URI + :type uri: string + """ matches = filter(lambda t: t.uri == uri, self._playlist.tracks) if matches: return matches[0] @@ -52,70 +94,164 @@ class BaseCurrentPlaylistController(object): raise KeyError('Track with URI "%s" not found' % uri) def load(self, playlist): + """ + Replace the current playlist with the given playlist. + + :param playlist: playlist to load + :type playlist: :class:`mopidy.models.Playlist` + """ self.playlist = playlist self.version = 0 def move(self, start, end, to_position): + """ + Move the tracks in the slice ``[start:end]`` to ``to_position``. + + :param start: position of first track to move + :type start: int + :param end: position after last track to move + :type end: int + :param to_position: new position for the tracks + :type to_position: int + """ tracks = self.playlist.tracks new_tracks = tracks[:start] + tracks[end:] - for track in tracks[start:end]: new_tracks.insert(to_position, track) to_position += 1 - self.playlist = self.playlist.with_(tracks=new_tracks) - def remove(self, position): + def remove(self, track): + """ + Remove the track from the current playlist. + + :param track: track to remove + :type track: :class:`mopidy.models.Track` + """ tracks = self.playlist.tracks + position = tracks.index(track) del tracks[position] self.playlist = self.playlist.with_(tracks=tracks) def shuffle(self, start=None, end=None): - tracks = self.playlist.tracks + """ + Shuffles the entire playlist. If ``start`` and ``end`` is given only + shuffles the slice ``[start:end]``. + :param start: position of first track to shuffle + :type start: int or :class:`None` + :param end: position after last track to shuffle + :type end: int or :class:`None` + """ + tracks = self.playlist.tracks before = tracks[:start or 0] shuffled = tracks[start:end] after = tracks[end or len(tracks):] - random.shuffle(shuffled) - self.playlist = self.playlist.with_(tracks=before+shuffled+after) class BaseLibraryController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + def __init__(self, backend): self.backend = backend def find_exact(self, type, query): + """ + Find tracks in the library where ``type`` matches ``query`` exactly. + + :param type: 'title', 'artist', or 'album' + :type type: string + :param query: the search query + :type query: string + :rtype: list of :class:`mopidy.models.Track` + """ raise NotImplementedError def lookup(self, uri): + """ + Lookup track with given URI. + + :param uri: track URI + :type uri: string + :rtype: :class:`mopidy.models.Track` + """ raise NotImplementedError def refresh(self, uri=None): + """ + Refresh library. Limit to URI and below if an URI is given. + + :param uri: directory or track URI + :type uri: string + """ raise NotImplementedError def search(self, type, query): + """ + Search the library for tracks where ``type`` contains ``query``. + + :param type: 'title', 'artist', 'album', or 'uri' + :type type: string + :param query: the search query + :type query: string + :rtype: list of :class:`mopidy.models.Track` + """ raise NotImplementedError class BasePlaybackController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + + #: Constant representing the paused state. PAUSED = u'paused' + + #: Constant representing the playing state. PLAYING = u'playing' + + #: Constant representing the stopped state. STOPPED = u'stopped' + #: :class:`True` + #: Tracks are removed from the playlist when they have been played. + #: :class:`False` + #: Tracks are not removed from the playlist. + consume = False + + #: The currently playing or selected :class:`mopidy.models.Track`. + current_track = None + + #: :class:`True` + #: Tracks are selected at random from the playlist. + #: :class:`False` + #: Tracks are played in the order of the playlist. + random = False + + #: :class:`True` + #: The current track is played repeatedly. + #: :class:`False` + #: The current track is played once. + repeat = False + + #: The audio volume as an int in the range [0, 100]. :class:`None` if + #: unknown. + volume = None + def __init__(self, backend): self.backend = backend self._state = self.STOPPED - self.consume = False - self.current_track = None - self.random = False - self.repeat = False self.state = self.STOPPED - self.volume = None @property def playlist_position(self): + """The position in the current playlist.""" if self.current_track is None: return None try: @@ -126,6 +262,10 @@ class BasePlaybackController(object): @property def state(self): + """ + The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or + :attr:`STOPPED`. + """ return self._state @state.setter @@ -142,6 +282,7 @@ class BasePlaybackController(object): @property def time_position(self): + """Time position in milliseconds.""" if self.state == self.PLAYING: time_since_started = int(time.time()) - self._play_time_started return self._play_time_accumulated + time_since_started @@ -162,6 +303,7 @@ class BasePlaybackController(object): self._play_time_started = int(time.time()) def new_playlist_loaded_callback(self): + """Tell the playback controller that a new playlist has been loaded.""" self.current_track = None if self.state == self.PLAYING: if self.backend.current_playlist.playlist.length > 0: @@ -170,6 +312,7 @@ class BasePlaybackController(object): self.stop() def next(self): + """Play the next track.""" self.stop() if self._next(): self.state = self.PLAYING @@ -178,6 +321,7 @@ class BasePlaybackController(object): raise NotImplementedError def pause(self): + """Pause playback.""" if self.state == self.PLAYING and self._pause(): self.state = self.PAUSED @@ -185,6 +329,12 @@ class BasePlaybackController(object): raise NotImplementedError def play(self, track=None): + """ + Play the given track or the currently active track. + + :param track: track to play + :type track: :class:`mopidy.models.Track` or :class:`None` + """ if self.state == self.PAUSED and track is None: return self.resume() if track is not None: @@ -197,6 +347,7 @@ class BasePlaybackController(object): raise NotImplementedError def previous(self): + """Play the previous track.""" self.stop() if self._previous(): self.state = self.PLAYING @@ -205,6 +356,7 @@ class BasePlaybackController(object): raise NotImplementedError def resume(self): + """If paused, resume playing the current track.""" if self.state == self.PAUSED and self._resume(): self.state = self.PLAYING @@ -212,9 +364,16 @@ class BasePlaybackController(object): raise NotImplementedError def seek(self, time_position): + """ + Seeks to time position given in milliseconds. + + :param time_position: time position in milliseconds + :type time_position: int + """ raise NotImplementedError def stop(self): + """Stop playing.""" if self.state != self.STOPPED and self._stop(): self.state = self.STOPPED @@ -223,34 +382,80 @@ class BasePlaybackController(object): class BaseStoredPlaylistsController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + def __init__(self, backend): self.backend = backend self._playlists = [] @property def playlists(self): + """List of :class:`mopidy.models.Playlist`.""" return copy(self._playlists) - def add(self, uri): - raise NotImplementedError - def create(self, name): + """ + Create a new playlist. + + :param name: name of the new playlist + :type name: string + :rtype: :class:`mopidy.models.Playlist` + """ raise NotImplementedError def delete(self, playlist): + """ + Delete playlist. + + :param playlist: the playlist to delete + :type playlist: :class:`mopidy.models.Playlist` + """ raise NotImplementedError def lookup(self, uri): + """ + Lookup playlist with given URI in both the set of stored playlists and + in any other playlist sources. + + :param uri: playlist URI + :type uri: string + :rtype: :class:`mopidy.models.Playlist` + """ raise NotImplementedError def refresh(self): + """Refresh stored playlists.""" raise NotImplementedError def rename(self, playlist, new_name): + """ + Rename playlist. + + :param playlist: the playlist + :type playlist: :class:`mopidy.models.Playlist` + :param new_name: the new name + :type new_name: string + """ raise NotImplementedError def save(self, playlist): + """ + Save the playlist to the set of stored playlists. + + :param playlist: the playlist + :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) From 82e7d0b9ac1b50594e117a07e61d03d6db19be2f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Feb 2010 13:12:25 +0100 Subject: [PATCH 16/51] Do not reset playlist version when loading a new playlist --- mopidy/backends/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 92e79000..210e8c19 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -33,7 +33,8 @@ class BaseCurrentPlaylistController(object): """ #: The current playlist version. Integer which is increased every time the - #: current playlist is changed. + #: current playlist is changed. Is not reset before the MPD server is + #: restarted. version = 0 def __init__(self, backend): @@ -101,7 +102,6 @@ class BaseCurrentPlaylistController(object): :type playlist: :class:`mopidy.models.Playlist` """ self.playlist = playlist - self.version = 0 def move(self, start, end, to_position): """ From 52aface8867f0829c11a86f9b36f2693f2d97db0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:09:23 +0100 Subject: [PATCH 17/51] Update MpdHandler._search() to do MPD output formatting --- mopidy/mpd/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index 41ea21ce..1df58186 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -360,7 +360,7 @@ class MpdHandler(object): @register(r'^search "(?P(album|artist|filename|title))" "(?P[^"]+)"$') def _search(self, type, what): - return self.backend.library.search(type, what) + return self.backend.library.search(type, what).mpd_format() @register(r'^seek "(?P\d+)" "(?P\d+)"$') def _seek(self, songpos, seconds): From 32afe4c1037bc73cf2a0f475fc4a13f27655bae8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:14:45 +0100 Subject: [PATCH 18/51] Add next_track and previous_track to BasePlaybackController API. Pass track object to internal _next() and _previous() methods. --- mopidy/backends/__init__.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 210e8c19..1706c0f6 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -249,6 +249,15 @@ class BasePlaybackController(object): self._state = self.STOPPED self.state = self.STOPPED + @property + def next_track(self): + """The next track in the playlist.""" + try: + return self.backend.current_playlist.playlist.tracks[ + self.playlist_position + 1] + except IndexError: + return None + @property def playlist_position(self): """The position in the current playlist.""" @@ -260,6 +269,15 @@ class BasePlaybackController(object): except ValueError: return None + @property + def previous_track(self): + """The previous track in the playlist.""" + try: + return self.backend.current_playlist.playlist.tracks[ + self.playlist_position - 1] + except IndexError: + return None + @property def state(self): """ @@ -314,10 +332,11 @@ class BasePlaybackController(object): def next(self): """Play the next track.""" self.stop() - if self._next(): + if self.next_track is not None and self._next(self.next_track): + self.current_track = self.next_track self.state = self.PLAYING - def _next(self): + def _next(self, track): raise NotImplementedError def pause(self): @@ -337,10 +356,9 @@ class BasePlaybackController(object): """ if self.state == self.PAUSED and track is None: return self.resume() - if track is not None: - self.current_track = track self.stop() - if self._play(track): + if track is not None and self._play(track): + self.current_track = track self.state = self.PLAYING def _play(self, track): @@ -349,10 +367,12 @@ class BasePlaybackController(object): def previous(self): """Play the previous track.""" self.stop() - if self._previous(): + if (self.previous_track is not None + and self._previous(self.previous_track)): + self.current_track = self.previous_track self.state = self.PLAYING - def _previous(self): + def _previous(self, track): raise NotImplementedError def resume(self): From a6cd24cc490b6986489ca6f770781e4ee73f1f1a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:18:29 +0100 Subject: [PATCH 19/51] BaseBackend's next(), play() and previous() should not call stop(). Wheter to stop before playing a new track is up to the backend to decide. --- mopidy/backends/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 1706c0f6..55f68794 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -331,7 +331,6 @@ class BasePlaybackController(object): def next(self): """Play the next track.""" - self.stop() if self.next_track is not None and self._next(self.next_track): self.current_track = self.next_track self.state = self.PLAYING @@ -356,7 +355,6 @@ class BasePlaybackController(object): """ if self.state == self.PAUSED and track is None: return self.resume() - self.stop() if track is not None and self._play(track): self.current_track = track self.state = self.PLAYING @@ -366,7 +364,6 @@ class BasePlaybackController(object): def previous(self): """Play the previous track.""" - self.stop() if (self.previous_track is not None and self._previous(self.previous_track)): self.current_track = self.previous_track From 9e59ef54e1708dbbfe62b2c749d6f63c2c059e5b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:20:44 +0100 Subject: [PATCH 20/51] Do not set initial state to STOPPED twice --- mopidy/backends/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 55f68794..c3dee726 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -247,7 +247,6 @@ class BasePlaybackController(object): def __init__(self, backend): self.backend = backend self._state = self.STOPPED - self.state = self.STOPPED @property def next_track(self): From f4c133b4d2e169f223ba23178c0a98adf72c9a5c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:23:44 +0100 Subject: [PATCH 21/51] Rewrite DespotifyBackend to use new backend API --- mopidy/backends/despotify.py | 157 ++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index a60e2aac..229f74dc 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -5,7 +5,9 @@ import sys import spytify from mopidy import config -from mopidy.backends import BaseBackend +from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BaseLibraryController, BasePlaybackController, + BaseStoredPlaylistsController) from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger(u'backends.despotify') @@ -14,36 +16,91 @@ ENCODING = 'utf-8' class DespotifyBackend(BaseBackend): def __init__(self, *args, **kwargs): - super(DespotifyBackend, self).__init__(*args, **kwargs) - logger.info(u'Connecting to Spotify') - self.spotify = spytify.Spytify( - config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD) - self.cache_stored_playlists() + self.current_playlist = DespotifyCurrentPlaylistController(backend=self) + self.library = DespotifyLibraryController(backend=self) + self.playback = DespotifyPlaybackController(backend=self) + self.stored_playlists = DespotifyStoredPlaylistsController(backend=self) + self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.translate = DespotifyTranslator() + self.spotify = self._connect() + self.stored_playlists.refresh() - def cache_stored_playlists(self): + def _connect(self): + logger.info(u'Connecting to Spotify') + return spytify.Spytify( + config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD) + + +class DespotifyCurrentPlaylistController(BaseCurrentPlaylistController): + pass + + +class DespotifyLibraryController(BaseLibraryController): + def search(self, type, what): + query = u'%s:%s' % (type, what) + result = self.backend.spotify.search(query.encode(ENCODING)) + if result is not None: + return self.backend.translate.to_mopidy_playlist(result.playlist) + + +class DespotifyPlaybackController(BasePlaybackController): + def _next(self, track): + return self._play(track) + + def _pause(self): + self.backend.spotify.pause() + return True + + def _play(self, track): + self.backend.spotify.play(self.backend.spotify.lookup(track.uri)) + return True + + def _previous(self, track): + return self._play(track) + + def _resume(self): + self.backend.spotify.resume() + return True + + def _stop(self): + self.backend.spotify.stop() + return True + + +class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController): + def refresh(self): logger.info(u'Caching stored playlists') playlists = [] - for spotify_playlist in self.spotify.stored_playlists: - playlists.append(self._to_mopidy_playlist(spotify_playlist)) + for spotify_playlist in self.backend.spotify.stored_playlists: + playlists.append( + self.backend.translate.to_mopidy_playlist(spotify_playlist)) self._playlists = playlists logger.debug(u'Available playlists: %s', - u', '.join([u'<%s>' % p.name for p in self._playlists])) + u', '.join([u'<%s>' % p.name + for p in self.backend.stored_playlists.playlists])) -# Model translation - def _to_mopidy_id(self, spotify_uri): - return 0 # TODO +class DespotifyTranslator(object): + uri_to_id_map = {} + next_id = 0 - def _to_mopidy_artist(self, spotify_artist): + def to_mopidy_id(self, spotify_uri): + if spotify_uri not in self.uri_to_id_map: + id = self.next_id + self.next_id += 1 + self.uri_to_id_map[spotify_uri] = id + return self.uri_to_id_map[spotify_uri] + + def to_mopidy_artist(self, spotify_artist): return Artist( uri=spotify_artist.get_uri(), name=spotify_artist.name.decode(ENCODING) ) - def _to_mopidy_album(self, spotify_album_name): + def to_mopidy_album(self, spotify_album_name): return Album(name=spotify_album_name.decode(ENCODING)) - def _to_mopidy_track(self, spotify_track): + def to_mopidy_track(self, spotify_track): if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR: date = dt.date(spotify_track.year, 1, 1) else: @@ -51,74 +108,18 @@ class DespotifyBackend(BaseBackend): return Track( uri=spotify_track.get_uri(), title=spotify_track.title.decode(ENCODING), - artists=[self._to_mopidy_artist(a) for a in spotify_track.artists], - album=self._to_mopidy_album(spotify_track.album), + artists=[self.to_mopidy_artist(a) for a in spotify_track.artists], + album=self.to_mopidy_album(spotify_track.album), track_no=spotify_track.tracknumber, date=date, length=spotify_track.length, - id=self._to_mopidy_id(spotify_track.get_uri()), + bitrate=320, + id=self.to_mopidy_id(spotify_track.get_uri()), ) - def _to_mopidy_playlist(self, spotify_playlist): + def to_mopidy_playlist(self, spotify_playlist): return Playlist( uri=spotify_playlist.get_uri(), name=spotify_playlist.name.decode(ENCODING), - tracks=[self._to_mopidy_track(t) for t in spotify_playlist.tracks], + tracks=[self.to_mopidy_track(t) for t in spotify_playlist.tracks], ) - -# Play control - - def _next(self): - self._current_song_pos += 1 - self.spotify.play(self.spotify.lookup(self._current_track.uri)) - return True - - def _pause(self): - self.spotify.pause() - return True - - def _play(self): - if self._current_track is not None: - self.spotify.play(self.spotify.lookup(self._current_track.uri)) - return True - else: - return False - - def _play_id(self, songid): - self._current_song_pos = songid # XXX - self.spotify.play(self.spotify.lookup(self._current_track.uri)) - return True - - def _play_pos(self, songpos): - self._current_song_pos = songpos - self.spotify.play(self.spotify.lookup(self._current_track.uri)) - return True - - def _previous(self): - self._current_song_pos -= 1 - self.spotify.play(self.spotify.lookup(self._current_track.uri)) - return True - - def _resume(self): - self.spotify.resume() - return True - - def _stop(self): - self.spotify.stop() - return True - -# Status querying - - def status_bitrate(self): - return 320 - - def url_handlers(self): - return [u'spotify:', u'http://open.spotify.com/'] - -# Music database - - def search(self, type, what): - query = u'%s:%s' % (type, what) - result = self.spotify.search(query.encode(ENCODING)) - if result is not None: - return self._to_mopidy_playlist(result.playlist).mpd_format() From a56e6fcd6edb32cf0840577b9dfc4af6efacd191 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 12:45:06 +0100 Subject: [PATCH 22/51] Rename id variable to this_id to not shadow global id() function --- mopidy/backends/despotify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 229f74dc..d95f0a3f 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -86,9 +86,9 @@ class DespotifyTranslator(object): def to_mopidy_id(self, spotify_uri): if spotify_uri not in self.uri_to_id_map: - id = self.next_id + this_id = self.next_id self.next_id += 1 - self.uri_to_id_map[spotify_uri] = id + self.uri_to_id_map[spotify_uri] = this_id return self.uri_to_id_map[spotify_uri] def to_mopidy_artist(self, spotify_artist): From fe4956ee4827e3c75b24891498a45fc2e72919bb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:00:09 +0100 Subject: [PATCH 23/51] despotify: Remove unused arguments --- mopidy/backends/despotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index d95f0a3f..cc826a63 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -15,7 +15,7 @@ logger = logging.getLogger(u'backends.despotify') ENCODING = 'utf-8' class DespotifyBackend(BaseBackend): - def __init__(self, *args, **kwargs): + def __init__(self): self.current_playlist = DespotifyCurrentPlaylistController(backend=self) self.library = DespotifyLibraryController(backend=self) self.playback = DespotifyPlaybackController(backend=self) From f8f46d4691be8d6306df6713c62d994300a57e0f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:00:22 +0100 Subject: [PATCH 24/51] despotify: Use shortest path to self.playlists --- mopidy/backends/despotify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index cc826a63..86860586 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -76,8 +76,7 @@ class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController): self.backend.translate.to_mopidy_playlist(spotify_playlist)) self._playlists = playlists logger.debug(u'Available playlists: %s', - u', '.join([u'<%s>' % p.name - for p in self.backend.stored_playlists.playlists])) + u', '.join([u'<%s>' % p.name for p in self.playlists])) class DespotifyTranslator(object): From a3ca1bf82fad6ccf371f157f09ad5dcbf060366b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:08:21 +0100 Subject: [PATCH 25/51] despotify: Remove unused import --- mopidy/backends/despotify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 86860586..abdf4a1d 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -1,6 +1,5 @@ import datetime as dt import logging -import sys import spytify From 580fca2cb6d48faccedffdc1447726d532a71e47 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:09:31 +0100 Subject: [PATCH 26/51] Add a couple of spaces --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index f6eb8326..0d5f29be 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -126,7 +126,7 @@ libspotify backend, copy the Spotify application key to ``mopidy/spotify_appkey.key``, and add the following to ``mopidy/mopidy/local_settings.py``:: - BACKEND=u'mopidy.backends.libspotify.LibspotifyBackend' + BACKEND = u'mopidy.backends.libspotify.LibspotifyBackend' To start Mopidy, go to the root of the Mopidy project, then simply run:: From 10534793aeca2d9b5db6224dca8668aef6aa2ef1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:14:13 +0100 Subject: [PATCH 27/51] libspotify: Rewrite backend to use new API --- mopidy/backends/libspotify.py | 183 ++++++++++++++++------------------ 1 file changed, 88 insertions(+), 95 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index d33875c9..638e0a34 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -1,4 +1,3 @@ -from copy import deepcopy import datetime as dt import logging import threading @@ -8,7 +7,9 @@ from spotify.manager import SpotifySessionManager from spotify.alsahelper import AlsaController from mopidy import config -from mopidy.backends import BaseBackend +from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BaseLibraryController, BasePlaybackController, + BaseStoredPlaylistsController) from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger(u'backends.libspotify') @@ -16,107 +17,50 @@ logger = logging.getLogger(u'backends.libspotify') ENCODING = 'utf-8' class LibspotifyBackend(BaseBackend): - def __init__(self, *args, **kwargs): - super(LibspotifyBackend, self).__init__(*args, **kwargs) - self._next_id = 0 - self._id_to_uri_map = {} - self._uri_to_id_map = {} + def __init__(self): + self.current_playlist = LibspotifyCurrentPlaylistController( + backend=self) + self.library = LibspotifyLibraryController(backend=self) + self.playback = LibspotifyPlaybackController(backend=self) + self.stored_playlists = LibspotifyStoredPlaylistsController( + backend=self) + self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.translate = LibspotifyTranslator() + self.spotify = self._connect() + self.stored_playlists.refresh() + + def _connect(self): logger.info(u'Connecting to Spotify') - self.spotify = LibspotifySessionManager( + spotify = LibspotifySessionManager( config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD, backend=self) - self.spotify.start() + spotify.start() + return spotify - def update_stored_playlists(self): - logger.info(u'Updating stored playlists') - playlists = [] - for spotify_playlist in self.spotify.playlists: - playlists.append(self._to_mopidy_playlist(spotify_playlist)) - self._playlists = playlists - logger.debug(u'Available playlists: %s', - u', '.join([u'<%s>' % p.name for p in self._playlists])) -# Model translation +class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController): + pass - def _to_mopidy_id(self, spotify_uri): - if spotify_uri in self._uri_to_id_map: - return self._uri_to_id_map[spotify_uri] - else: - id = self._next_id - self._next_id += 1 - self._id_to_uri_map[id] = spotify_uri - self._uri_to_id_map[spotify_uri] = id - return id - def _to_mopidy_artist(self, spotify_artist): - return Artist( - uri=str(Link.from_artist(spotify_artist)), - name=spotify_artist.name().decode(ENCODING), - ) +class LibspotifyLibraryController(BaseLibraryController): + pass - def _to_mopidy_album(self, spotify_album): - # TODO pyspotify got much more data on albums than this - return Album(name=spotify_album.name().decode(ENCODING)) - def _to_mopidy_track(self, spotify_track): - return Track( - uri=str(Link.from_track(spotify_track, 0)), - title=spotify_track.name().decode(ENCODING), - artists=[self._to_mopidy_artist(a) - for a in spotify_track.artists()], - album=self._to_mopidy_album(spotify_track.album()), - track_no=spotify_track.index(), - date=dt.date(spotify_track.album().year(), 1, 1), - length=spotify_track.duration(), - id=self._to_mopidy_id(str(Link.from_track(spotify_track, 0))), - ) - - def _to_mopidy_playlist(self, spotify_playlist): - return Playlist( - uri=str(Link.from_playlist(spotify_playlist)), - name=spotify_playlist.name().decode(ENCODING), - tracks=[self._to_mopidy_track(t) for t in spotify_playlist], - ) -# Playback control - - def _play_current_track(self): - self.spotify.session.load( - Link.from_string(self._current_track.uri).as_track()) - self.spotify.session.play(1) - - def _next(self): - self._current_song_pos += 1 - self._play_current_track() - return True +class LibspotifyPlaybackController(BasePlaybackController): + def _next(self, track): + return self._play(track) def _pause(self): # TODO return False - def _play(self): - if self._current_track is not None: - self._play_current_track() - return True - else: - return False - - def _play_id(self, songid): - matches = filter(lambda t: t.id == songid, self._current_playlist) - if matches: - self._current_song_pos = self._current_playlist.index(matches[0]) - self._play_current_track() - return True - else: - return False - - def _play_pos(self, songpos): - self._current_song_pos = songpos - self._play_current_track() + def _play(self, track): + self.backend.spotify.session.load( + Link.from_string(self._current_track.uri).as_track()) + self.backend.spotify.session.play(1) return True - def _previous(self): - self._current_song_pos -= 1 - self._play_current_track() - return True + def _previous(self, track): + return self._play(track) def _resume(self): # TODO @@ -126,13 +70,60 @@ class LibspotifyBackend(BaseBackend): self.spotify.session.play(0) return True -# Status querying - def status_bitrate(self): - return 320 +class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): + def refresh(self): + logger.info(u'Refreshing stored playlists') + playlists = [] + for spotify_playlist in self.backend.spotify.playlists: + playlists.append( + self.backend.translate.to_mopidy_playlist(spotify_playlist)) + self._playlists = playlists + logger.debug(u'Available playlists: %s', + u', '.join([u'<%s>' % p.name for p in self.playlists])) - def url_handlers(self): - return [u'spotify:', u'http://open.spotify.com/'] + +class LibspotifyTranslator(object): + uri_to_id_map = {} + next_id = 0 + + def to_mopidy_id(self, spotify_uri): + if spotify_uri not in self.uri_to_id_map: + this_id = self.next_id + self.next_id += 1 + self.uri_to_id_map[spotify_uri] = this_id + return self._uri_to_id_map[spotify_uri] + + def to_mopidy_artist(self, spotify_artist): + return Artist( + uri=str(Link.from_artist(spotify_artist)), + name=spotify_artist.name().decode(ENCODING), + ) + + def to_mopidy_album(self, spotify_album): + # TODO pyspotify got much more data on albums than this + return Album(name=spotify_album.name().decode(ENCODING)) + + def to_mopidy_track(self, spotify_track): + uri = str(Link.from_track(spotify_track, 0)) + return Track( + uri=uri, + title=spotify_track.name().decode(ENCODING), + artists=[self.to_mopidy_artist(a) for a in spotify_track.artists()], + album=self.to_mopidy_album(spotify_track.album()), + track_no=spotify_track.index(), + date=dt.date(spotify_track.album().year(), 1, 1), + length=spotify_track.duration(), + bitrate=320, + id=self.to_mopidy_id(uri), + ) + + def to_mopidy_playlist(self, spotify_playlist): + return Playlist( + uri=str(Link.from_playlist(spotify_playlist)), + name=spotify_playlist.name().decode(ENCODING), + tracks=[self.to_mopidy_track(t) for t in spotify_playlist], + ) class LibspotifySessionManager(SpotifySessionManager, threading.Thread): @@ -141,6 +132,7 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): threading.Thread.__init__(self) self.backend = backend self.audio = AlsaController() + self.playlists = [] def run(self): self.connect() @@ -159,7 +151,9 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def metadata_updated(self, session): logger.debug('Metadata updated') - self.backend.update_stored_playlists() + # XXX This changes data "owned" by another thread, and leads to + # segmentation fault. We should use locking and messaging here. + self.backend.stored_playlists.refresh() def connection_error(self, session, error): logger.error('Connection error: %s', error) @@ -181,4 +175,3 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def end_of_track(self, session): logger.debug('End of track') - From 09b8319f0a155e73fc9a4cfea67441f843d13a08 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 13:39:54 +0100 Subject: [PATCH 28/51] Give BaseBackend's _next() and _previous() a default implementation --- mopidy/backends/__init__.py | 4 ++-- mopidy/backends/despotify.py | 6 ------ mopidy/backends/libspotify.py | 6 ------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index c3dee726..49e9790c 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -335,7 +335,7 @@ class BasePlaybackController(object): self.state = self.PLAYING def _next(self, track): - raise NotImplementedError + return self._play(track) def pause(self): """Pause playback.""" @@ -369,7 +369,7 @@ class BasePlaybackController(object): self.state = self.PLAYING def _previous(self, track): - raise NotImplementedError + return self._play(track) def resume(self): """If paused, resume playing the current track.""" diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index abdf4a1d..85f5abee 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -43,9 +43,6 @@ class DespotifyLibraryController(BaseLibraryController): class DespotifyPlaybackController(BasePlaybackController): - def _next(self, track): - return self._play(track) - def _pause(self): self.backend.spotify.pause() return True @@ -54,9 +51,6 @@ class DespotifyPlaybackController(BasePlaybackController): self.backend.spotify.play(self.backend.spotify.lookup(track.uri)) return True - def _previous(self, track): - return self._play(track) - def _resume(self): self.backend.spotify.resume() return True diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 638e0a34..588ad175 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -46,9 +46,6 @@ class LibspotifyLibraryController(BaseLibraryController): class LibspotifyPlaybackController(BasePlaybackController): - def _next(self, track): - return self._play(track) - def _pause(self): # TODO return False @@ -59,9 +56,6 @@ class LibspotifyPlaybackController(BasePlaybackController): self.backend.spotify.session.play(1) return True - def _previous(self, track): - return self._play(track) - def _resume(self): # TODO return False From 45e5b3fd3e0d6457a05b759505d7211b7c9def86 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 17:38:21 +0100 Subject: [PATCH 29/51] Refer to the Track class in next_track/previous_track docs --- mopidy/backends/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 49e9790c..70bfa0eb 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -250,7 +250,7 @@ class BasePlaybackController(object): @property def next_track(self): - """The next track in the playlist.""" + """The next :class:`mopidy.models.Track` in the playlist.""" try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position + 1] @@ -270,7 +270,7 @@ class BasePlaybackController(object): @property def previous_track(self): - """The previous track in the playlist.""" + """The previous :class:`mopidy.models.Track` in the playlist.""" try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position - 1] From 200d6a22ca0db7a5090d85b03afcbd1ff1c87338 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 17:40:53 +0100 Subject: [PATCH 30/51] DummyLibraryController.search() should return a Playlist --- mopidy/backends/dummy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index d88e3a0b..643d97c0 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -1,6 +1,7 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, BasePlaybackController, BaseLibraryController, BaseStoredPlaylistsController) +from mopidy.models import Playlist class DummyBackend(BaseBackend): def __init__(self): @@ -15,7 +16,7 @@ class DummyCurrentPlaylistController(BaseCurrentPlaylistController): class DummyLibraryController(BaseLibraryController): def search(self, type, query): - return [] + return Playlist() class DummyPlaybackController(BasePlaybackController): def _next(self): From ac13b20667f98980cfdbec5ae9eaee300b04c4f2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2010 17:43:35 +0100 Subject: [PATCH 31/51] next_track/previous_track should be None if current_track is None --- mopidy/backends/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 70bfa0eb..0ebc389c 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -251,6 +251,8 @@ class BasePlaybackController(object): @property def next_track(self): """The next :class:`mopidy.models.Track` in the playlist.""" + if self.current_track is None: + return None try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position + 1] @@ -271,6 +273,8 @@ class BasePlaybackController(object): @property def previous_track(self): """The previous :class:`mopidy.models.Track` in the playlist.""" + if self.current_track is None: + return None try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position - 1] From 379694ac4eaec0000fae7b6cff994f91d78620ce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Feb 2010 22:47:23 +0100 Subject: [PATCH 32/51] despotify: search() should always return a Playlist --- mopidy/backends/despotify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 85f5abee..dc786d64 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -38,8 +38,9 @@ class DespotifyLibraryController(BaseLibraryController): def search(self, type, what): query = u'%s:%s' % (type, what) result = self.backend.spotify.search(query.encode(ENCODING)) - if result is not None: - return self.backend.translate.to_mopidy_playlist(result.playlist) + if result is None: + return Playlist() + return self.backend.translate.to_mopidy_playlist(result.playlist) class DespotifyPlaybackController(BasePlaybackController): From c8fa0c4dcfe5de650098bbd64da8c330d8943ef8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Feb 2010 22:49:06 +0100 Subject: [PATCH 33/51] libspotify: Fix typo in field name --- mopidy/backends/libspotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 588ad175..df299fb2 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -86,7 +86,7 @@ class LibspotifyTranslator(object): this_id = self.next_id self.next_id += 1 self.uri_to_id_map[spotify_uri] = this_id - return self._uri_to_id_map[spotify_uri] + return self.uri_to_id_map[spotify_uri] def to_mopidy_artist(self, spotify_artist): return Artist( From 80b1f377b604dac1799e04d019851340f758d1b9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Feb 2010 22:59:59 +0100 Subject: [PATCH 34/51] Fix broken MpdHandler tests --- tests/mpd/handlertest.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index c130725d..ef1e114e 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -295,19 +295,29 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_pause_off(self): - self.h.handle_request(u'play') + track = Track() + self.b.current_playlist.playlist = Playlist(tracks=[track]) + self.b.playback.current_track = track + self.h.handle_request(u'play "0"') self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') self.assert_(u'OK' in result) self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) def test_pause_on(self): - self.h.handle_request(u'play') + track = Track() + self.b.current_playlist.playlist = Playlist(tracks=[track]) + self.b.playback.current_track = track + self.h.handle_request(u'play "0"') result = self.h.handle_request(u'pause "1"') self.assert_(u'OK' in result) self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) def test_play_without_pos(self): + track = Track() + self.b.current_playlist.playlist = Playlist(tracks=[track]) + self.b.playback.current_track = track + self.b.playback.state = self.b.playback.PAUSED result = self.h.handle_request(u'play') self.assert_(u'OK' in result) self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) From e3e5f0c9bb852bae85f1f5e6afba11de63182a68 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Feb 2010 23:33:28 +0100 Subject: [PATCH 35/51] Add tests to get full test coverage of MpdHandler --- mopidy/backends/dummy.py | 3 +- mopidy/mpd/handler.py | 21 +++---- tests/mpd/handlertest.py | 118 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 122 insertions(+), 20 deletions(-) diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index 643d97c0..93c8f218 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -35,4 +35,5 @@ class DummyPlaybackController(BasePlaybackController): return True class DummyStoredPlaylistsController(BaseStoredPlaylistsController): - pass + def search(self, query): + return [Playlist(name=query)] diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index 1df58186..23803059 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -394,15 +394,14 @@ class MpdHandler(object): @register(r'^stats$') def _stats(self): - pass # TODO return { - 'artists': 0, - 'albums': 0, - 'songs': 0, + 'artists': 0, # TODO + 'albums': 0, # TODO + 'songs': 0, # TODO 'uptime': self.session.stats_uptime(), - 'db_playtime': 0, - 'db_update': 0, - 'playtime': 0, + 'db_playtime': 0, # TODO + 'db_update': 0, # TODO + 'playtime': 0, # TODO } @register(r'^stop$') @@ -487,10 +486,12 @@ class MpdHandler(object): return self.backend.playback.time_position def _status_time_total(self): - if self.backend.playback.current_track is not None: - return self.backend.playback.current_track.length // 1000 - else: + if self.backend.playback.current_track is None: return 0 + elif self.backend.playback.current_track.length is None: + return 0 + else: + return self.backend.playback.current_track.length // 1000 def _status_volume(self): if self.backend.playback.volume is not None: diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index ef1e114e..e9f3ec19 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -144,38 +144,116 @@ class StatusHandlerTest(unittest.TestCase): result = self.h.handle_request(u'status') self.assert_(u'OK' in result) - def test_status_method(self): + def test_status_method_contains_volume_which_defaults_to_0(self): + self.b.playback.volume = None result = dict(self.h._status()) self.assert_('volume' in result) - self.assert_(int(result['volume']) in xrange(0, 101)) + self.assertEquals(int(result['volume']), 0) + + def test_status_method_contains_volume(self): + self.b.playback.volume = 17 + result = dict(self.h._status()) + self.assert_('volume' in result) + self.assertEquals(int(result['volume']), 17) + + def test_status_method_contains_repeat_is_0(self): + result = dict(self.h._status()) self.assert_('repeat' in result) - self.assert_(int(result['repeat']) in (0, 1)) + self.assertEquals(int(result['repeat']), 0) + + def test_status_method_contains_repeat_is_1(self): + self.b.playback.repeat = 1 + result = dict(self.h._status()) + self.assert_('repeat' in result) + self.assertEquals(int(result['repeat']), 1) + + def test_status_method_contains_random_is_0(self): + result = dict(self.h._status()) self.assert_('random' in result) - self.assert_(int(result['random']) in (0, 1)) + self.assertEquals(int(result['random']), 0) + + def test_status_method_contains_random_is_1(self): + self.b.playback.random = 1 + result = dict(self.h._status()) + self.assert_('random' in result) + self.assertEquals(int(result['random']), 1) + + def test_status_method_contains_single(self): + result = dict(self.h._status()) self.assert_('single' in result) self.assert_(int(result['single']) in (0, 1)) + + def test_status_method_contains_consume_is_0(self): + result = dict(self.h._status()) self.assert_('consume' in result) - self.assert_(int(result['consume']) in (0, 1)) + self.assertEquals(int(result['consume']), 0) + + def test_status_method_contains_consume_is_1(self): + self.b.playback.consume = 1 + result = dict(self.h._status()) + self.assert_('consume' in result) + self.assertEquals(int(result['consume']), 1) + + def test_status_method_contains_playlist(self): + result = dict(self.h._status()) self.assert_('playlist' in result) self.assert_(int(result['playlist']) in xrange(0, 2**31)) + + def test_status_method_contains_playlistlength(self): + result = dict(self.h._status()) self.assert_('playlistlength' in result) self.assert_(int(result['playlistlength']) >= 0) + + def test_status_method_contains_xfade(self): + result = dict(self.h._status()) self.assert_('xfade' in result) self.assert_(int(result['xfade']) >= 0) - self.assert_('state' in result) - self.assert_(result['state'] in ('play', 'stop', 'pause')) - def test_status_method_when_playlist_loaded(self): + def test_status_method_contains_state_is_play(self): + self.b.playback.state = self.b.playback.PLAYING + result = dict(self.h._status()) + self.assert_('state' in result) + self.assertEquals(result['state'], 'play') + + def test_status_method_contains_state_is_stop(self): + self.b.playback.state = self.b.playback.STOPPED + result = dict(self.h._status()) + self.assert_('state' in result) + self.assertEquals(result['state'], 'stop') + + def test_status_method_contains_state_is_pause(self): + self.b.playback.state = self.b.playback.PLAYING + self.b.playback.state = self.b.playback.PAUSED + result = dict(self.h._status()) + self.assert_('state' in result) + self.assertEquals(result['state'], 'pause') + + def test_status_method_when_playlist_loaded_contains_song(self): track = Track() self.b.current_playlist.load(Playlist(tracks=[track])) self.b.playback.current_track = track result = dict(self.h._status()) self.assert_('song' in result) self.assert_(int(result['song']) >= 0) + + def test_status_method_when_playlist_loaded_contains_pos_as_songid(self): + track = Track() + self.b.current_playlist.load(Playlist(tracks=[track])) + self.b.playback.current_track = track + result = dict(self.h._status()) self.assert_('songid' in result) self.assert_(int(result['songid']) >= 0) - def test_status_method_when_playing(self): + def test_status_method_when_playlist_loaded_contains_id_as_songid(self): + track = Track(id=1) + self.b.current_playlist.load(Playlist(tracks=[track])) + self.b.playback.current_track = track + result = dict(self.h._status()) + self.assert_('songid' in result) + self.assertEquals(int(result['songid']), 1) + + def test_status_method_when_playing_contains_time_with_no_length(self): + self.b.playback.current_track = Track(length=None) self.b.playback.state = self.b.playback.PLAYING result = dict(self.h._status()) self.assert_('time' in result) @@ -184,6 +262,23 @@ class StatusHandlerTest(unittest.TestCase): total = int(total) self.assert_(position <= total) + def test_status_method_when_playing_contains_time_with_length(self): + self.b.playback.current_track = Track(length=10000) + self.b.playback.state = self.b.playback.PLAYING + result = dict(self.h._status()) + self.assert_('time' in result) + (position, total) = result['time'].split(':') + position = int(position) + total = int(total) + self.assert_(position <= total) + + def test_status_method_when_playing_contains_bitrate(self): + self.b.playback.state = self.b.playback.PLAYING + self.b.playback.current_track = Track(bitrate=320) + result = dict(self.h._status()) + self.assert_('bitrate' in result) + self.assertEquals(int(result['bitrate']), 320) + class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): @@ -340,6 +435,11 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + def test_playid_which_does_not_exist(self): + self.b.current_playlist.load(Playlist(tracks=[Track(id=0)])) + result = self.h.handle_request(u'playid "1"') + self.assert_(u'ACK Track with ID "1" not found' in result) + def test_previous(self): result = self.h.handle_request(u'previous') self.assert_(u'OK' in result) From 3784bee5831d8ab7a775bc1145bd1d2b0ab212d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 01:20:27 +0100 Subject: [PATCH 36/51] Library search should return Playlist, not list --- mopidy/backends/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 0ebc389c..8b6de67c 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -199,7 +199,7 @@ class BaseLibraryController(object): :type type: string :param query: the search query :type query: string - :rtype: list of :class:`mopidy.models.Track` + :rtype: :class:`mopidy.models.Playlist` """ raise NotImplementedError From 52c52da33ebf8dac8bb213f33e7ebd0d4364dc97 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 01:25:36 +0100 Subject: [PATCH 37/51] libspotify: Check is_loaded() on all Spotify objects before using them --- mopidy/backends/libspotify.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index df299fb2..ea491441 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -89,16 +89,22 @@ class LibspotifyTranslator(object): return self.uri_to_id_map[spotify_uri] def to_mopidy_artist(self, spotify_artist): + if not spotify_artist.is_loaded(): + return Artist(name=u'[loading...]') return Artist( uri=str(Link.from_artist(spotify_artist)), name=spotify_artist.name().decode(ENCODING), ) def to_mopidy_album(self, spotify_album): + if not spotify_album.is_loaded(): + return Album(name=u'[loading...]') # TODO pyspotify got much more data on albums than this return Album(name=spotify_album.name().decode(ENCODING)) def to_mopidy_track(self, spotify_track): + if not spotify_track.is_loaded(): + return Track(title=u'[loading...]') uri = str(Link.from_track(spotify_track, 0)) return Track( uri=uri, @@ -113,6 +119,8 @@ class LibspotifyTranslator(object): ) def to_mopidy_playlist(self, spotify_playlist): + if not spotify_playlist.is_loaded(): + return Playlist(name=u'[loading...]') return Playlist( uri=str(Link.from_playlist(spotify_playlist)), name=spotify_playlist.name().decode(ENCODING), From 051b79e7cbb35b71890b850cbc010e8cc5e21947 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 01:27:24 +0100 Subject: [PATCH 38/51] libspotify: Add dummy library search --- mopidy/backends/libspotify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index ea491441..e6c74863 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -42,7 +42,8 @@ class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController): class LibspotifyLibraryController(BaseLibraryController): - pass + def search(self, type, query): + return Playlist() # TODO class LibspotifyPlaybackController(BasePlaybackController): From ba7a3a51e34dc710ed30153a881ba86348fd8af2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 01:28:39 +0100 Subject: [PATCH 39/51] libspotify: Improve playback controls by stopping before playing new track. Fix a couple of bugs. --- mopidy/backends/libspotify.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index e6c74863..14615ee6 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -47,22 +47,38 @@ class LibspotifyLibraryController(BaseLibraryController): class LibspotifyPlaybackController(BasePlaybackController): + def _next(self, track): + if self.state == self.PLAYING: + self._stop() + self._play(track) + return True + def _pause(self): # TODO return False def _play(self, track): + if self.state == self.PLAYING: + self._stop() + if track.uri is None: + return False self.backend.spotify.session.load( - Link.from_string(self._current_track.uri).as_track()) + Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) return True + def _previous(self, track): + if self.state == self.PLAYING: + self._stop() + self._play(track) + return True + def _resume(self): # TODO return False def _stop(self): - self.spotify.session.play(0) + self.backend.spotify.session.play(0) return True From 5386a8102ec7893ff04840a169d11d4015ebfd1b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 01:29:19 +0100 Subject: [PATCH 40/51] libspotify: Play next track on end of track. Stop playback when losing play token. --- mopidy/backends/libspotify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 14615ee6..676e883d 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -188,9 +188,11 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def play_token_lost(self, session): logger.debug('Play token lost') + self.backend.playback.stop() def log_message(self, session, data): logger.debug(data) def end_of_track(self, session): logger.debug('End of track') + self.backend.playback.next() From 81c7f56b5b5922eec596336d4c4d93ef58193c9d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 14:10:30 +0100 Subject: [PATCH 41/51] Add tests for mopidy.models --- tests/__main__.py | 1 + tests/modelstest.py | 160 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 tests/modelstest.py diff --git a/tests/__main__.py b/tests/__main__.py index 86c814ac..e203582a 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -9,6 +9,7 @@ def main(): sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) r = CoverageTestRunner() + r.add_pair('mopidy/models.py', 'tests/modelstest.py') r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py') r.run() diff --git a/tests/modelstest.py b/tests/modelstest.py new file mode 100644 index 00000000..df20f05e --- /dev/null +++ b/tests/modelstest.py @@ -0,0 +1,160 @@ +import datetime as dt +import unittest + +from mopidy.models import Artist, Album, Track, Playlist + +class ArtistTest(unittest.TestCase): + def test_uri(self): + uri = u'an_uri' + artist = Artist(uri=uri) + self.assertEqual(artist.uri, uri) + + def test_name(self): + name = u'a name' + artist = Artist(name=name) + self.assertEqual(artist.name, name) + + +class AlbumTest(unittest.TestCase): + def test_uri(self): + uri = u'an_uri' + album = Album(uri=uri) + self.assertEqual(album.uri, uri) + + def test_name(self): + name = u'a name' + album = Album(name=name) + self.assertEqual(album.name, name) + + def test_artists(self): + artists = [Artist()] + album = Album(artists=artists) + self.assertEqual(album.artists, artists) + + def test_num_tracks(self): + num_tracks = 11 + album = Album(num_tracks=11) + self.assertEqual(album.num_tracks, num_tracks) + + +class TrackTest(unittest.TestCase): + def test_uri(self): + uri = u'an_uri' + track = Track(uri=uri) + self.assertEqual(track.uri, uri) + + def test_title(self): + title = u'a title' + track = Track(title=title) + self.assertEqual(track.title, title) + + def test_artists(self): + artists = [Artist(), Artist()] + track = Track(artists=artists) + self.assertEqual(track.artists, artists) + + def test_album(self): + album = Album() + track = Track(album=album) + self.assertEqual(track.album, album) + + def test_track_no(self): + track_no = 7 + track = Track(track_no=track_no) + self.assertEqual(track.track_no, track_no) + + def test_date(self): + date = dt.date(1977, 1, 1) + track = Track(date=date) + self.assertEqual(track.date, date) + + def test_length(self): + length = 137000 + track = Track(length=length) + self.assertEqual(track.length, length) + + def test_bitrate(self): + bitrate = 160 + track = Track(bitrate=bitrate) + self.assertEqual(track.bitrate, bitrate) + + def test_id(self): + id = 17 + track = Track(id=id) + self.assertEqual(track.id, id) + + def test_mpd_format_for_empty_track(self): + track = Track() + result = track.mpd_format() + self.assert_(('file', '') in result) + self.assert_(('Time', 0) in result) + self.assert_(('Artist', '') in result) + self.assert_(('Title', '') in result) + self.assert_(('Album', '') in result) + self.assert_(('Track', '0/0') in result) + self.assert_(('Date', '') in result) + self.assert_(('Pos', 0) in result) + self.assert_(('Id', 0) in result) + + def test_mpd_format_artists(self): + track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) + self.assertEqual(track.mpd_format_artists(), u'ABBA, Beatles') + +class PlaylistTest(unittest.TestCase): + def test_uri(self): + uri = u'an_uri' + playlist = Playlist(uri=uri) + self.assertEqual(playlist.uri, uri) + + def test_name(self): + name = u'a name' + playlist = Playlist(name=name) + self.assertEqual(playlist.name, name) + + def test_tracks(self): + tracks = [Track(), Track(), Track()] + playlist = Playlist(tracks=tracks) + self.assertEqual(playlist.tracks, tracks) + + def test_length(self): + tracks = [Track(), Track(), Track()] + playlist = Playlist(tracks=tracks) + self.assertEqual(playlist.length, 3) + + def test_mpd_format(self): + playlist = Playlist(tracks=[ + Track(track_no=1), Track(track_no=2), Track(track_no=3)]) + result = playlist.mpd_format() + self.assertEqual(len(result), 3) + + def test_mpd_format_with_range(self): + playlist = Playlist(tracks=[ + Track(track_no=1), Track(track_no=2), Track(track_no=3)]) + result = playlist.mpd_format(1, 2) + self.assertEqual(len(result), 1) + self.assertEqual(dict(result[0])['Track'], '2/0') + + def test_with_new_uri(self): + tracks = [Track()] + playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks) + new_playlist = playlist.with_(uri=u'another uri') + self.assertEqual(new_playlist.uri, u'another uri') + self.assertEqual(new_playlist.name, u'a name') + self.assertEqual(new_playlist.tracks, tracks) + + def test_with_new_name(self): + tracks = [Track()] + playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks) + new_playlist = playlist.with_(name=u'another name') + self.assertEqual(new_playlist.uri, u'an uri') + self.assertEqual(new_playlist.name, u'another name') + self.assertEqual(new_playlist.tracks, tracks) + + def test_with_new_tracks(self): + tracks = [Track()] + playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks) + new_tracks = [Track(), Track()] + new_playlist = playlist.with_(tracks=new_tracks) + self.assertEqual(new_playlist.uri, u'an uri') + self.assertEqual(new_playlist.name, u'a name') + self.assertEqual(new_playlist.tracks, new_tracks) From 902953d99259b621b0ca2f69d17bd5563b3defbc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 14:10:50 +0100 Subject: [PATCH 42/51] Fix bugs in mopidy.models revealed by new tests --- mopidy/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 8880651e..3c96aa74 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -9,7 +9,7 @@ class Artist(object): """ def __init__(self, uri=None, name=None): - self._uri = None + self._uri = uri self._name = name @property @@ -221,7 +221,7 @@ class Playlist(object): if end is None: end = self.length tracks = [] - for track, position in zip(self.tracks, range(start, end)): + for track, position in zip(self.tracks[start:end], range(start, end)): tracks.append(track.mpd_format(position)) return tracks From 837e7c361c95557b36cbdbb15f905ff95cf6b6d6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 14:43:28 +0100 Subject: [PATCH 43/51] Test that all model fields are immutable --- tests/modelstest.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/modelstest.py b/tests/modelstest.py index df20f05e..aaa2ee1e 100644 --- a/tests/modelstest.py +++ b/tests/modelstest.py @@ -8,11 +8,13 @@ class ArtistTest(unittest.TestCase): uri = u'an_uri' artist = Artist(uri=uri) self.assertEqual(artist.uri, uri) + self.assertRaises(AttributeError, setattr, artist, 'uri', None) def test_name(self): name = u'a name' artist = Artist(name=name) self.assertEqual(artist.name, name) + self.assertRaises(AttributeError, setattr, artist, 'name', None) class AlbumTest(unittest.TestCase): @@ -20,21 +22,25 @@ class AlbumTest(unittest.TestCase): uri = u'an_uri' album = Album(uri=uri) self.assertEqual(album.uri, uri) + self.assertRaises(AttributeError, setattr, album, 'uri', None) def test_name(self): name = u'a name' album = Album(name=name) self.assertEqual(album.name, name) + self.assertRaises(AttributeError, setattr, album, 'name', None) def test_artists(self): artists = [Artist()] album = Album(artists=artists) self.assertEqual(album.artists, artists) + self.assertRaises(AttributeError, setattr, album, 'artists', None) def test_num_tracks(self): num_tracks = 11 album = Album(num_tracks=11) self.assertEqual(album.num_tracks, num_tracks) + self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) class TrackTest(unittest.TestCase): @@ -42,46 +48,55 @@ class TrackTest(unittest.TestCase): uri = u'an_uri' track = Track(uri=uri) self.assertEqual(track.uri, uri) + self.assertRaises(AttributeError, setattr, track, 'uri', None) def test_title(self): title = u'a title' track = Track(title=title) self.assertEqual(track.title, title) + self.assertRaises(AttributeError, setattr, track, 'title', None) def test_artists(self): artists = [Artist(), Artist()] track = Track(artists=artists) self.assertEqual(track.artists, artists) + self.assertRaises(AttributeError, setattr, track, 'artists', None) def test_album(self): album = Album() track = Track(album=album) self.assertEqual(track.album, album) + self.assertRaises(AttributeError, setattr, track, 'album', None) def test_track_no(self): track_no = 7 track = Track(track_no=track_no) self.assertEqual(track.track_no, track_no) + self.assertRaises(AttributeError, setattr, track, 'track_no', None) def test_date(self): date = dt.date(1977, 1, 1) track = Track(date=date) self.assertEqual(track.date, date) + self.assertRaises(AttributeError, setattr, track, 'date', None) def test_length(self): length = 137000 track = Track(length=length) self.assertEqual(track.length, length) + self.assertRaises(AttributeError, setattr, track, 'length', None) def test_bitrate(self): bitrate = 160 track = Track(bitrate=bitrate) self.assertEqual(track.bitrate, bitrate) + self.assertRaises(AttributeError, setattr, track, 'bitrate', None) def test_id(self): id = 17 track = Track(id=id) self.assertEqual(track.id, id) + self.assertRaises(AttributeError, setattr, track, 'id', None) def test_mpd_format_for_empty_track(self): track = Track() @@ -105,16 +120,19 @@ class PlaylistTest(unittest.TestCase): uri = u'an_uri' playlist = Playlist(uri=uri) self.assertEqual(playlist.uri, uri) + self.assertRaises(AttributeError, setattr, playlist, 'uri', None) def test_name(self): name = u'a name' playlist = Playlist(name=name) self.assertEqual(playlist.name, name) + self.assertRaises(AttributeError, setattr, playlist, 'name', None) def test_tracks(self): tracks = [Track(), Track(), Track()] playlist = Playlist(tracks=tracks) self.assertEqual(playlist.tracks, tracks) + self.assertRaises(AttributeError, setattr, playlist, 'tracks', None) def test_length(self): tracks = [Track(), Track(), Track()] From 94b100d7af26dcb242782bb109c53948dfc81d52 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 15:42:17 +0100 Subject: [PATCH 44/51] Simplify models by changing __setattr__ instead of having tons of property getters --- mopidy/models.py | 154 ++++++++++++++++++----------------------------- 1 file changed, 59 insertions(+), 95 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 3c96aa74..2fb0d909 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,6 +1,15 @@ from copy import copy -class Artist(object): +class ImmutableObject(object): + def __init__(self, *args, **kwargs): + self.__dict__.update(kwargs) + + def __setattr__(self, name, value): + if name.startswith('_'): + return super(ImmutableObject, self).__setattr__(name, value) + raise AttributeError('Object is immutable.') + +class Artist(ImmutableObject): """ :param uri: artist URI :type uri: string @@ -8,22 +17,14 @@ class Artist(object): :type name: string """ - def __init__(self, uri=None, name=None): - self._uri = uri - self._name = name + #: The artist URI. Read-only. + uri = None - @property - def uri(self): - """The artist URI. Read-only.""" - return self._uri - - @property - def name(self): - """The artist name. Read-only.""" - return self._name + #: The artist name. Read-only. + name = None -class Album(object): +class Album(ImmutableObject): """ :param uri: album URI :type uri: string @@ -35,34 +36,26 @@ class Album(object): :type num_tracks: integer """ - def __init__(self, uri=None, name=None, artists=None, num_tracks=0): - self._uri = uri - self._name = name - self._artists = artists or [] - self._num_tracks = num_tracks + #: The album URI. Read-only. + uri = None - @property - def uri(self): - """The album URI. Read-only.""" - return self._uri + #: The album name. Read-only.""" + name = None - @property - def name(self): - """The album name. Read-only.""" - return self._name + #: The number of tracks in the album. Read-only. + num_tracks = 0 + + def __init__(self, *args, **kwargs): + self._artists = kwargs.pop('artists', []) + super(Album, self).__init__(*args, **kwargs) @property def artists(self): """List of :class:`Artist` elements. Read-only.""" return copy(self._artists) - @property - def num_tracks(self): - """The number of tracks in the album. Read-only.""" - return self._num_tracks - -class Track(object): +class Track(ImmutableObject): """ :param uri: track URI :type uri: string @@ -84,63 +77,39 @@ class Track(object): :type id: integer """ - def __init__(self, uri=None, title=None, artists=None, album=None, - track_no=0, date=None, length=None, bitrate=None, id=None): - self._uri = uri - self._title = title - self._artists = artists or [] - self._album = album - self._track_no = track_no - self._date = date - self._length = length - self._bitrate = bitrate - self._id = id + #: The track URI. Read-only. + uri = None - @property - def uri(self): - """The track URI. Read-only.""" - return self._uri + #: The track title. Read-only. + title = None - @property - def title(self): - """The track title. Read-only.""" - return self._title + #: The track :class:`Album`. Read-only. + album = None + + #: The track number in album. Read-only. + track_no = 0 + + #: The track release date. Read-only. + date = None + + #: The track length in milliseconds. Read-only. + length = None + + #: The track's bitrate in kbit/s. Read-only. + bitrate = None + + #: The track ID. Read-only. + id = None + + def __init__(self, *args, **kwargs): + self._artists = kwargs.pop('artists', []) + super(Track, self).__init__(*args, **kwargs) @property def artists(self): """List of :class:`Artist`. Read-only.""" return copy(self._artists) - @property - def album(self): - """The track :class:`Album`. Read-only.""" - return self._album - - @property - def track_no(self): - """The track number in album. Read-only.""" - return self._track_no - - @property - def date(self): - """The track release date. Read-only.""" - return self._date - - @property - def length(self): - """The track length in milliseconds. Read-only.""" - return self._length - - @property - def bitrate(self): - """The track's bitrate in kbit/s. Read-only.""" - return self._bitrate - - @property - def id(self): - """The track ID. Read-only.""" - return self._id - def mpd_format(self, position=0): """ Format track for output to MPD client. @@ -171,7 +140,7 @@ class Track(object): return u', '.join([a.name for a in self.artists]) -class Playlist(object): +class Playlist(ImmutableObject): """ :param uri: playlist URI :type uri: string @@ -181,20 +150,15 @@ class Playlist(object): :type tracks: list of :class:`Track` elements """ - def __init__(self, uri=None, name=None, tracks=None): - self._uri = uri - self._name = name - self._tracks = tracks or [] + #: The playlist URI. Read-only. + uri = None - @property - def uri(self): - """The playlist URI. Read-only.""" - return self._uri + #: The playlist name. Read-only. + name = None - @property - def name(self): - """The playlist name. Read-only.""" - return self._name + def __init__(self, *args, **kwargs): + self._tracks = kwargs.pop('tracks', []) + super(Playlist, self).__init__(*args, **kwargs) @property def tracks(self): From a098704334de090ce833e99e71230d88ee37d713 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 15:50:18 +0100 Subject: [PATCH 45/51] Remove redundant docstring end --- mopidy/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/models.py b/mopidy/models.py index 2fb0d909..64dffd29 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -39,7 +39,7 @@ class Album(ImmutableObject): #: The album URI. Read-only. uri = None - #: The album name. Read-only.""" + #: The album name. Read-only. name = None #: The number of tracks in the album. Read-only. From 8adbacae098005f0054df709f2d50a7ee1aae468 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 16:03:37 +0100 Subject: [PATCH 46/51] libspotify: Stored playlists are refreshed when we get the updated metadata callback --- mopidy/backends/libspotify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 676e883d..93065fa7 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -27,7 +27,6 @@ class LibspotifyBackend(BaseBackend): self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] self.translate = LibspotifyTranslator() self.spotify = self._connect() - self.stored_playlists.refresh() def _connect(self): logger.info(u'Connecting to Spotify') From e72166ad789afac5e652c288d861dec302383353 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 16:14:13 +0100 Subject: [PATCH 47/51] libspotify: Remove again redundant _next and _previous overrides. Let _play call stop instead of _stop. --- mopidy/backends/libspotify.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 93065fa7..d1942f3b 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -46,19 +46,13 @@ class LibspotifyLibraryController(BaseLibraryController): class LibspotifyPlaybackController(BasePlaybackController): - def _next(self, track): - if self.state == self.PLAYING: - self._stop() - self._play(track) - return True - def _pause(self): # TODO return False def _play(self, track): if self.state == self.PLAYING: - self._stop() + self.stop() if track.uri is None: return False self.backend.spotify.session.load( @@ -66,12 +60,6 @@ class LibspotifyPlaybackController(BasePlaybackController): self.backend.spotify.session.play(1) return True - def _previous(self, track): - if self.state == self.PLAYING: - self._stop() - self._play(track) - return True - def _resume(self): # TODO return False From 0a3341063b092a977cee91d5e3a0140ae4957be8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 17:41:22 +0100 Subject: [PATCH 48/51] Format search results correctly --- mopidy/models.py | 21 +++++++++++++-------- mopidy/mpd/handler.py | 3 ++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 64dffd29..86561bc3 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -110,7 +110,7 @@ class Track(ImmutableObject): """List of :class:`Artist`. Read-only.""" return copy(self._artists) - def mpd_format(self, position=0): + def mpd_format(self, position=0, search_result=False): """ Format track for output to MPD client. @@ -118,18 +118,23 @@ class Track(ImmutableObject): :type position: integer :rtype: list of two-tuples """ - return [ + result = [ ('file', self.uri or ''), ('Time', self.length and (self.length // 1000) or 0), ('Artist', self.mpd_format_artists()), ('Title', self.title or ''), ('Album', self.album and self.album.name or ''), - ('Track', '%d/%d' % (self.track_no, - self.album and self.album.num_tracks or 0)), ('Date', self.date or ''), - ('Pos', position), - ('Id', self.id or position), ] + if self.album is not None and self.album.num_tracks != 0: + result.append(('Track', '%d/%d' % ( + self.track_no, self.album.num_tracks))) + else: + result.append(('Track', self.track_no)) + if not search_result: + result.append(('Pos', position)) + result.append(('Id', self.id or position)) + return result def mpd_format_artists(self): """ @@ -170,7 +175,7 @@ class Playlist(ImmutableObject): """The number of tracks in the playlist. Read-only.""" return len(self._tracks) - def mpd_format(self, start=0, end=None): + def mpd_format(self, start=0, end=None, search_result=False): """ Format playlist for output to MPD client. @@ -186,7 +191,7 @@ class Playlist(ImmutableObject): end = self.length tracks = [] for track, position in zip(self.tracks[start:end], range(start, end)): - tracks.append(track.mpd_format(position)) + tracks.append(track.mpd_format(position, search_result)) return tracks def with_(self, uri=None, name=None, tracks=None): diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index 23803059..f3395fd8 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -360,7 +360,8 @@ class MpdHandler(object): @register(r'^search "(?P(album|artist|filename|title))" "(?P[^"]+)"$') def _search(self, type, what): - return self.backend.library.search(type, what).mpd_format() + return self.backend.library.search(type, what).mpd_format( + search_result=True) @register(r'^seek "(?P\d+)" "(?P\d+)"$') def _seek(self, songpos, seconds): From 552b3ac723fcf95606ef205fb4d4310db4f89543 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 19:58:19 +0100 Subject: [PATCH 49/51] Fix tests and coverage for track status output --- tests/modelstest.py | 26 ++++++++++++++++++++++++-- tests/mpd/handlertest.py | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/modelstest.py b/tests/modelstest.py index aaa2ee1e..29911dc7 100644 --- a/tests/modelstest.py +++ b/tests/modelstest.py @@ -106,11 +106,33 @@ class TrackTest(unittest.TestCase): self.assert_(('Artist', '') in result) self.assert_(('Title', '') in result) self.assert_(('Album', '') in result) - self.assert_(('Track', '0/0') in result) + self.assert_(('Track', 0) in result) self.assert_(('Date', '') in result) self.assert_(('Pos', 0) in result) self.assert_(('Id', 0) in result) + def test_mpd_format_for_nonempty_track(self): + track = Track( + uri=u'a uri', + artists=[Artist(name=u'an artist')], + title=u'a title', + album=Album(name=u'an album', num_tracks=13), + track_no=7, + date=dt.date(1977, 1, 1), + length=137000, + id=122, + ) + result = track.mpd_format(position=9) + self.assert_(('file', 'a uri') in result) + self.assert_(('Time', 137) in result) + self.assert_(('Artist', 'an artist') in result) + self.assert_(('Title', 'a title') in result) + self.assert_(('Album', 'an album') in result) + self.assert_(('Track', '7/13') in result) + self.assert_(('Date', dt.date(1977, 1, 1)) in result) + self.assert_(('Pos', 9) in result) + self.assert_(('Id', 122) in result) + def test_mpd_format_artists(self): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) self.assertEqual(track.mpd_format_artists(), u'ABBA, Beatles') @@ -150,7 +172,7 @@ class PlaylistTest(unittest.TestCase): Track(track_no=1), Track(track_no=2), Track(track_no=3)]) result = playlist.mpd_format(1, 2) self.assertEqual(len(result), 1) - self.assertEqual(dict(result[0])['Track'], '2/0') + self.assertEqual(dict(result[0])['Track'], 2) def test_with_new_uri(self): tracks = [Track()] diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index e9f3ec19..a6716bc2 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -101,7 +101,7 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'Artist: ' in result) self.assert_(u'Title: ' in result) self.assert_(u'Album: ' in result) - self.assert_(u'Track: 0/0' in result) + self.assert_(u'Track: 0' in result) self.assert_(u'Date: ' in result) self.assert_(u'Pos: 0' in result) self.assert_(u'Id: 0' in result) From 01a6751ce69e5a078d3f3b89de67467258d62731 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 20:11:42 +0100 Subject: [PATCH 50/51] Add support for 'outputs' MPD command --- mopidy/mpd/handler.py | 8 ++++++++ tests/mpd/handlertest.py | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index f3395fd8..ef7274f8 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -220,6 +220,14 @@ class MpdHandler(object): def _next(self): return self.backend.playback.next() + @register(r'^outputs$') + def _outputs(self): + return [ + ('outputid', 0), + ('outputname', self.backend.__class__.__name__), + ('outputenabled', 1), + ] + @register(r'^password "(?P[^"]+)"$') def _password(self, password): raise MpdNotImplemented # TODO diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index a6716bc2..a9a9d653 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -774,11 +774,17 @@ class ConnectionHandlerTest(unittest.TestCase): result = self.h.handle_request(u'ping') self.assert_(u'OK' in result) + class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): self.h = handler.MpdHandler(backend=DummyBackend()) - pass # TODO + def test_outputs(self): + result = self.h.handle_request(u'outputs') + self.assert_(u'outputid: 0' in result) + self.assert_(u'outputname: DummyBackend' in result) + self.assert_(u'outputenabled: 1' in result) + self.assert_(u'OK' in result) class ReflectionHandlerTest(unittest.TestCase): From f59a0a042d853ce21be66580bfcf50d518d98efa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Feb 2010 20:30:33 +0100 Subject: [PATCH 51/51] libspotify: Implement search --- mopidy/backends/libspotify.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index d1942f3b..b27333fd 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -1,6 +1,7 @@ import datetime as dt import logging import threading +import time from spotify import Link from spotify.manager import SpotifySessionManager @@ -41,8 +42,22 @@ class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController): class LibspotifyLibraryController(BaseLibraryController): - def search(self, type, query): - return Playlist() # TODO + search_results = False + + def search(self, type, what): + # XXX This is slow + self.search_results = None + def callback(results, userdata): + logger.debug(u'Search results received') + self.search_results = results + query = u'%s:%s' % (type, what) + self.backend.spotify.search(query.encode(ENCODING), callback) + while self.search_results is None: + time.sleep(0.01) + result = Playlist(tracks=[self.backend.translate.to_mopidy_track(t) + for t in self.search_results.tracks()]) + self.search_results = False + return result class LibspotifyPlaybackController(BasePlaybackController): @@ -183,3 +198,6 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def end_of_track(self, session): logger.debug('End of track') self.backend.playback.next() + + def search(self, query, callback): + self.session.search(query, callback)