diff --git a/docs/development.rst b/docs/development.rst index 2a39b327..826dd215 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -24,10 +24,46 @@ modular, so we can extend it with other backends in the future, like file playback and other online music services such as Last.fm. +Code style +========== + +We generally follow the `PEP-8 `_ +style guidelines, with a couple of notable exceptions: + +- We indent continuation lines with four spaces more than the previous line. + For example:: + + from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BasePlaybackController, BaseLibraryController, + BaseStoredPlaylistsController) + + And not:: + + from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BasePlaybackController, BaseLibraryController, + BaseStoredPlaylistsController) + +- An exception to the previous exception: When continuing control flow + statements like ``if``, ``for`` and ``while``, we indent with eight spaces + more than the previous line. In other words, the line is indented one level + further to the right than the following block of code. For example:: + + if (old_state in (self.PLAYING, self.STOPPED) + and new_state == self.PLAYING): + self._play_time_start() + + And not:: + + if (old_state in (self.PLAYING, self.STOPPED) + and new_state == self.PLAYING): + self._play_time_start() + + Running tests ============= -To run tests, you need a couple of dependiencies. Some can be installed through Debian/Ubuntu package management:: +To run tests, you need a couple of dependencies. Some can be installed through +Debian/Ubuntu package management:: sudo aptitude install python-coverage diff --git a/docs/installation.rst b/docs/installation.rst index 0d5f29be..f924fd72 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,34 +23,58 @@ Dependencies .. _despotify: despotify backend -================= +----------------- To use the despotify backend, you first need to install despotify and spytify. -*This backend requires a Spotify premium account.* +.. note:: + + This backend requires a Spotify premium account. -Installing despotify and spytify --------------------------------- +Installing despotify +^^^^^^^^^^^^^^^^^^^^ -Install despotify's dependencies. At Debian/Ubuntu systems:: +*Linux:* Install despotify's dependencies. At Debian/Ubuntu systems:: sudo aptitude install libssl-dev zlib1g-dev libvorbis-dev \ libtool libncursesw5-dev libao-dev -Check out revision 503 of the despotify source code:: +*OS X:* In OS X you need to have `XCode +`_ and `MacPorts +`_ installed. Then, to install despotify's +dependencies:: - svn co https://despotify.svn.sourceforge.net/svnroot/despotify@503 despotify + sudo port install openssl zlib libvorbis libtool ncursesw libao -Build and install despotify:: +*All OS:* Check out revision 503 of the despotify source code:: + + svn co https://despotify.svn.sourceforge.net/svnroot/despotify@503 + +*OS X:* Edit ``despotify/src/Makefile.local.mk`` and uncomment the last two +lines so that it reads:: + + ## If you're on Mac OS X and have installed libvorbisfile + ## via 'port install ..', try uncommenting these lines + CFLAGS += -I/opt/local/include + LDFLAGS += -L/opt/local/lib + +*All OS:* Build and install despotify:: cd despotify/src/ make sudo make install + +Installing spytify +^^^^^^^^^^^^^^^^^^ + +spytify's source comes bundled with despotify. + Build and install spytify:: cd despotify/src/bindings/python/ + export PKG_CONFIG_PATH=../../lib # Needed on OS X make sudo make install @@ -64,22 +88,24 @@ Spotify Premium account), ask for a search query, list all your playlists with tracks, play 10s from a random song from the search result, pause for two seconds, play for five more seconds, and quit. + .. _libspotify: libspotify backend -================== +------------------ As an alternative to the despotify backend, we are working on a libspotify backend. To use the libspotify backend you must install libspotify and pyspotify. -*This backend requires a Spotify premium account.* +.. note:: -*This backend requires you to get an application key from Spotify before use.* + This backend requires a Spotify premium account, and it requires you to get + an application key from Spotify before use. -Installing libspotify and pyspotify ------------------------------------ +Installing libspotify +^^^^^^^^^^^^^^^^^^^^^ As libspotify's installation script at the moment is somewhat broken (see this `GetSatisfaction thread `_ @@ -87,6 +113,10 @@ for details), it is easiest to use the libspotify files bundled with pyspotify. The files bundled with pyspotify are for 64-bit, so if you run a 32-bit OS, you must get libspotify from https://developer.spotify.com/en/libspotify/. + +Installing pyspotify +^^^^^^^^^^^^^^^^^^^^ + Install pyspotify's dependencies. At Debian/Ubuntu systems:: sudo aptitude install python-alsaaudio @@ -106,13 +136,15 @@ Test your libspotify setup:: ./example1.py -u USERNAME -p PASSWORD -Until Spotify fixes their installation script, you'll have to set -``LD_LIBRARY_PATH`` every time you are going to use libspotify (in other words -before starting Mopidy). +.. note:: + + Until Spotify fixes their installation script, you'll have to set + ``LD_LIBRARY_PATH`` every time you are going to use libspotify (in other + words before starting Mopidy). -Running Mopidy -============== +Settings +======== Create a file name ``local_settings.py`` in the same directory as ``settings.py``. Enter your Spotify Premium account's username and password @@ -128,6 +160,9 @@ libspotify backend, copy the Spotify application key to BACKEND = u'mopidy.backends.libspotify.LibspotifyBackend' +Running Mopidy +============== + To start Mopidy, go to the root of the Mopidy project, then simply run:: python mopidy diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 8b6de67c..962e91ba 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -74,6 +74,7 @@ class BaseCurrentPlaylistController(object): :param id: track ID :type id: int + :rtype: :class:`mopidy.models.Track` """ matches = filter(lambda t: t.id == id, self._playlist.tracks) if matches: @@ -87,6 +88,7 @@ class BaseCurrentPlaylistController(object): :param uri: track URI :type uri: string + :rtype: :class:`mopidy.models.Track` """ matches = filter(lambda t: t.uri == uri, self._playlist.tracks) if matches: diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index b27333fd..dfe40609 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -1,7 +1,6 @@ import datetime as dt import logging import threading -import time from spotify import Link from spotify.manager import SpotifySessionManager @@ -42,21 +41,23 @@ class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController): class LibspotifyLibraryController(BaseLibraryController): - search_results = False + _search_results = None + _search_results_received = threading.Event() def search(self, type, what): - # XXX This is slow - self.search_results = None + # FIXME When searching while playing music, this is really slow, like + # 12-14s between querying and getting results. + self._search_results_received.clear() + query = u'%s:%s' % (type, what) def callback(results, userdata): logger.debug(u'Search results received') - self.search_results = results - query = u'%s:%s' % (type, what) + self._search_results = results + self._search_results_received.set() self.backend.spotify.search(query.encode(ENCODING), callback) - while self.search_results is None: - time.sleep(0.01) + self._search_results_received.wait() result = Playlist(tracks=[self.backend.translate.to_mopidy_track(t) - for t in self.search_results.tracks()]) - self.search_results = False + for t in self._search_results.tracks()]) + self._search_results = None return result diff --git a/mopidy/models.py b/mopidy/models.py index 86561bc3..934ca8ac 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,6 +1,14 @@ from copy import copy class ImmutableObject(object): + """ + Superclass for immutable objects whose fields can only be modified via the + constructor. + + :param kwargs: kwargs to set as fields on the object + :type kwargs: any + """ + def __init__(self, *args, **kwargs): self.__dict__.update(kwargs) diff --git a/mopidy/mpd/handler.py b/mopidy/mpd/handler.py index ef7274f8..d9ba1713 100644 --- a/mopidy/mpd/handler.py +++ b/mopidy/mpd/handler.py @@ -66,10 +66,19 @@ class MpdHandler(object): response.append(u'%s: %s' % (key, value)) else: response.append(line) - if add_ok: + if add_ok and (not response or not response[-1].startswith(u'ACK')): response.append(u'OK') return response + @register(r'^ack$') + def _ack(self): + """ + Always returns an 'ACK' and not 'OK'. + + Not a part of the MPD protocol. + """ + raise MpdNotImplemented + @register(r'^add "(?P[^"]*)"$') def _add(self, uri): raise MpdNotImplemented # TODO @@ -109,10 +118,16 @@ class MpdHandler(object): response = self.handle_request(command, add_ok=False) if response is not None: result.append(response) + if response and response[-1].startswith(u'ACK'): + return result if command_list_ok: response.append(u'list_OK') return result + @register(r'^commands$') + def _commands(self): + raise MpdNotImplemented # TODO + @register(r'^consume "(?P[01])"$') def _consume(self, state): state = int(state) @@ -133,7 +148,12 @@ class MpdHandler(object): @register(r'^currentsong$') def _currentsong(self): if self.backend.playback.current_track is not None: - return self.backend.playback.current_track.mpd_format() + return self.backend.playback.current_track.mpd_format( + position=self.backend.playback.playlist_position) + + @register(r'^decoders$') + def _decoders(self): + raise MpdNotImplemented # TODO @register(r'^delete "(?P\d+)"$') @register(r'^delete "(?P\d+):(?P\d+)*"$') @@ -142,12 +162,25 @@ class MpdHandler(object): @register(r'^deleteid "(?P\d+)"$') def _deleteid(self, songid): + songid = int(songid) + try: + track = self.backend.current_playlist.get_by_id(songid) + return self.backend.current_playlist.remove(track) + except KeyError, e: + raise MpdAckError(unicode(e)) + + @register(r'^disableoutput "(?P\d+)"$') + def _disableoutput(self, outputid): raise MpdNotImplemented # TODO @register(r'^$') def _empty(self): pass + @register(r'^enableoutput "(?P\d+)"$') + def _enableoutput(self, outputid): + raise MpdNotImplemented # TODO + @register(r'^find "(?P(album|artist|title))" "(?P[^"]+)"$') def _find(self, type, what): raise MpdNotImplemented # TODO @@ -220,6 +253,10 @@ class MpdHandler(object): def _next(self): return self.backend.playback.next() + @register(r'^notcommands$') + def _notcommands(self): + raise MpdNotImplemented # TODO + @register(r'^outputs$') def _outputs(self): return [ @@ -511,6 +548,26 @@ class MpdHandler(object): def _status_xfade(self): return 0 # TODO + @register(r'^sticker delete "(?P[^"]+)" "(?P[^"]+)"( "(?P[^"]+)")*$') + def _sticker_delete(self, type, uri, name=None): + raise MpdNotImplemented # TODO + + @register(r'^sticker find "(?P[^"]+)" "(?P[^"]+)" "(?P[^"]+)"$') + def sticker_find(self, type, uri, name): + raise MpdNotImplemented # TODO + + @register(r'^sticker get "(?P[^"]+)" "(?P[^"]+)" "(?P[^"]+)"$') + def _sticker_get(self, type, uri, name): + raise MpdNotImplemented # TODO + + @register(r'^sticker list "(?P[^"]+)" "(?P[^"]+)"$') + def _sticker_list(self, type, uri): + raise MpdNotImplemented # TODO + + @register(r'^sticker set "(?P[^"]+)" "(?P[^"]+)" "(?P[^"]+)" "(?P[^"]+)"$') + def _sticker_set(self, type, uri, name, value): + raise MpdNotImplemented # TODO + @register(r'^swap "(?P\d+)" "(?P\d+)"$') def _swap(self, songpos1, songpos2): raise MpdNotImplemented # TODO @@ -519,6 +576,10 @@ class MpdHandler(object): def _swapid(self, songid1, songid2): raise MpdNotImplemented # TODO + @register(r'^tagtypes$') + def _tagtypes(self): + raise MpdNotImplemented # TODO + @register(r'^update( "(?P[^"]+)")*$') def _update(self, uri=None, rescan_unmodified_files=False): return {'updating_db': 0} # TODO diff --git a/mopidy/mpd/session.py b/mopidy/mpd/session.py index edd2a95c..c85ed3a0 100644 --- a/mopidy/mpd/session.py +++ b/mopidy/mpd/session.py @@ -48,7 +48,8 @@ class MpdSession(asynchat.async_chat): def handle_request(self, input): try: response = self.handler.handle_request(input) - self.handle_response(response) + if response is not None: + self.handle_response(response) except MpdAckError, e: logger.warning(e) return self.send_response(u'ACK %s' % e) diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index a9a9d653..8996d6af 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -66,6 +66,12 @@ class CommandListsTest(unittest.TestCase): self.assert_(u'OK' in result) self.assertEquals(False, self.h.command_list) + def test_command_list_with_error(self): + self.h.handle_request(u'command_list_begin') + self.h.handle_request(u'ack') + result = self.h.handle_request(u'command_list_end') + self.assert_(u'ACK' in result[-1]) + def test_command_list_ok_begin(self): result = self.h.handle_request(u'command_list_ok_begin') self.assert_(result is None) @@ -94,7 +100,9 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'ACK Not implemented' in result) def test_currentsong(self): - self.b.playback.current_track = Track() + track = Track() + self.b.current_playlist.playlist = Playlist(tracks=[track]) + 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) @@ -460,7 +468,8 @@ class PlaybackControlHandlerTest(unittest.TestCase): class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.b = DummyBackend() + self.h = handler.MpdHandler(backend=self.b) def test_add(self): result = self.h.handle_request(u'add "file:///dev/urandom"') @@ -491,8 +500,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'ACK Not implemented' in result) def test_deleteid(self): + self.b.current_playlist.load(Playlist(tracks=[Track(id=0)])) result = self.h.handle_request(u'deleteid "0"') - self.assert_(u'ACK Not implemented' in result) + self.assert_(u'OK' in result) + + def test_deleteid_does_not_exist(self): + result = self.h.handle_request(u'deleteid "0"') + self.assert_(u'ACK Track with ID "0" not found' in result) def test_move_songpos(self): result = self.h.handle_request(u'move "5" "0"') @@ -746,7 +760,35 @@ class StickersHandlerTest(unittest.TestCase): def setUp(self): self.h = handler.MpdHandler(backend=DummyBackend()) - pass # TODO + def test_sticker_get(self): + result = self.h.handle_request( + u'sticker get "song" "file:///dev/urandom" "a_name"') + self.assert_(u'ACK Not implemented' in result) + + def test_sticker_set(self): + result = self.h.handle_request( + u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"') + self.assert_(u'ACK Not implemented' in result) + + def test_sticker_delete_with_name(self): + result = self.h.handle_request( + u'sticker delete "song" "file:///dev/urandom" "a_name"') + self.assert_(u'ACK Not implemented' in result) + + def test_sticker_delete_without_name(self): + result = self.h.handle_request( + u'sticker delete "song" "file:///dev/urandom"') + self.assert_(u'ACK Not implemented' in result) + + def test_sticker_list(self): + result = self.h.handle_request( + u'sticker list "song" "file:///dev/urandom"') + self.assert_(u'ACK Not implemented' in result) + + def test_sticker_find(self): + result = self.h.handle_request( + u'sticker find "song" "file:///dev/urandom" "a_name"') + self.assert_(u'ACK Not implemented' in result) class ConnectionHandlerTest(unittest.TestCase): @@ -779,6 +821,14 @@ class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): self.h = handler.MpdHandler(backend=DummyBackend()) + def test_enableoutput(self): + result = self.h.handle_request(u'enableoutput "0"') + self.assert_(u'ACK Not implemented' in result) + + def test_disableoutput(self): + result = self.h.handle_request(u'disableoutput "0"') + self.assert_(u'ACK Not implemented' in result) + def test_outputs(self): result = self.h.handle_request(u'outputs') self.assert_(u'outputid: 0' in result) @@ -791,10 +841,24 @@ class ReflectionHandlerTest(unittest.TestCase): def setUp(self): self.h = handler.MpdHandler(backend=DummyBackend()) + def test_commands(self): + result = self.h.handle_request(u'commands') + self.assert_(u'ACK Not implemented' in result) + + def test_decoders(self): + result = self.h.handle_request(u'decoders') + self.assert_(u'ACK Not implemented' in result) + + def test_notcommands(self): + result = self.h.handle_request(u'notcommands') + self.assert_(u'ACK Not implemented' in result) + + def test_tagtypes(self): + result = self.h.handle_request(u'tagtypes') + self.assert_(u'ACK Not implemented' in result) + def test_urlhandlers(self): result = self.h.handle_request(u'urlhandlers') self.assert_(u'OK' in result) result = result[0] self.assert_('dummy:' in result) - - pass # TODO