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