Merge branch 'master' of git://github.com/jodal/mopidy

This commit is contained in:
Thomas Adamcik 2010-02-20 00:14:23 +01:00
commit 45af7ef21e
8 changed files with 246 additions and 38 deletions

View File

@ -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. playback and other online music services such as Last.fm.
Code style
==========
We generally follow the `PEP-8 <http://www.python.org/dev/peps/pep-0008/>`_
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 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 sudo aptitude install python-coverage

View File

@ -23,34 +23,58 @@ Dependencies
.. _despotify: .. _despotify:
despotify backend despotify backend
================= -----------------
To use the despotify backend, you first need to install despotify and spytify. 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 \ sudo aptitude install libssl-dev zlib1g-dev libvorbis-dev \
libtool libncursesw5-dev libao-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
<http://developer.apple.com/tools/xcode/>`_ and `MacPorts
<http://www.macports.org/>`_ 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/ cd despotify/src/
make make
sudo make install sudo make install
Installing spytify
^^^^^^^^^^^^^^^^^^
spytify's source comes bundled with despotify.
Build and install spytify:: Build and install spytify::
cd despotify/src/bindings/python/ cd despotify/src/bindings/python/
export PKG_CONFIG_PATH=../../lib # Needed on OS X
make make
sudo make install 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 tracks, play 10s from a random song from the search result, pause for two
seconds, play for five more seconds, and quit. seconds, play for five more seconds, and quit.
.. _libspotify: .. _libspotify:
libspotify backend libspotify backend
================== ------------------
As an alternative to the despotify backend, we are working on a libspotify As an alternative to the despotify backend, we are working on a libspotify
backend. To use the libspotify backend you must install libspotify and backend. To use the libspotify backend you must install libspotify and
pyspotify. 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 As libspotify's installation script at the moment is somewhat broken (see this
`GetSatisfaction thread <http://getsatisfaction.com/spotify/topics/libspotify_please_fix_the_installation_script>`_ `GetSatisfaction thread <http://getsatisfaction.com/spotify/topics/libspotify_please_fix_the_installation_script>`_
@ -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 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/. must get libspotify from https://developer.spotify.com/en/libspotify/.
Installing pyspotify
^^^^^^^^^^^^^^^^^^^^
Install pyspotify's dependencies. At Debian/Ubuntu systems:: Install pyspotify's dependencies. At Debian/Ubuntu systems::
sudo aptitude install python-alsaaudio sudo aptitude install python-alsaaudio
@ -106,13 +136,15 @@ Test your libspotify setup::
./example1.py -u USERNAME -p PASSWORD ./example1.py -u USERNAME -p PASSWORD
Until Spotify fixes their installation script, you'll have to set .. note::
``LD_LIBRARY_PATH`` every time you are going to use libspotify (in other words
before starting Mopidy). 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 Create a file name ``local_settings.py`` in the same directory as
``settings.py``. Enter your Spotify Premium account's username and password ``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' BACKEND = u'mopidy.backends.libspotify.LibspotifyBackend'
Running Mopidy
==============
To start Mopidy, go to the root of the Mopidy project, then simply run:: To start Mopidy, go to the root of the Mopidy project, then simply run::
python mopidy python mopidy

View File

@ -74,6 +74,7 @@ class BaseCurrentPlaylistController(object):
:param id: track ID :param id: track ID
:type id: int :type id: int
:rtype: :class:`mopidy.models.Track`
""" """
matches = filter(lambda t: t.id == id, self._playlist.tracks) matches = filter(lambda t: t.id == id, self._playlist.tracks)
if matches: if matches:
@ -87,6 +88,7 @@ class BaseCurrentPlaylistController(object):
:param uri: track URI :param uri: track URI
:type uri: string :type uri: string
:rtype: :class:`mopidy.models.Track`
""" """
matches = filter(lambda t: t.uri == uri, self._playlist.tracks) matches = filter(lambda t: t.uri == uri, self._playlist.tracks)
if matches: if matches:

View File

@ -1,7 +1,6 @@
import datetime as dt import datetime as dt
import logging import logging
import threading import threading
import time
from spotify import Link from spotify import Link
from spotify.manager import SpotifySessionManager from spotify.manager import SpotifySessionManager
@ -42,21 +41,23 @@ class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController):
class LibspotifyLibraryController(BaseLibraryController): class LibspotifyLibraryController(BaseLibraryController):
search_results = False _search_results = None
_search_results_received = threading.Event()
def search(self, type, what): def search(self, type, what):
# XXX This is slow # FIXME When searching while playing music, this is really slow, like
self.search_results = None # 12-14s between querying and getting results.
self._search_results_received.clear()
query = u'%s:%s' % (type, what)
def callback(results, userdata): def callback(results, userdata):
logger.debug(u'Search results received') logger.debug(u'Search results received')
self.search_results = results self._search_results = results
query = u'%s:%s' % (type, what) self._search_results_received.set()
self.backend.spotify.search(query.encode(ENCODING), callback) self.backend.spotify.search(query.encode(ENCODING), callback)
while self.search_results is None: self._search_results_received.wait()
time.sleep(0.01)
result = Playlist(tracks=[self.backend.translate.to_mopidy_track(t) result = Playlist(tracks=[self.backend.translate.to_mopidy_track(t)
for t in self.search_results.tracks()]) for t in self._search_results.tracks()])
self.search_results = False self._search_results = None
return result return result

View File

@ -1,6 +1,14 @@
from copy import copy from copy import copy
class ImmutableObject(object): 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): def __init__(self, *args, **kwargs):
self.__dict__.update(kwargs) self.__dict__.update(kwargs)

View File

@ -66,10 +66,19 @@ class MpdHandler(object):
response.append(u'%s: %s' % (key, value)) response.append(u'%s: %s' % (key, value))
else: else:
response.append(line) response.append(line)
if add_ok: if add_ok and (not response or not response[-1].startswith(u'ACK')):
response.append(u'OK') response.append(u'OK')
return response 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<uri>[^"]*)"$') @register(r'^add "(?P<uri>[^"]*)"$')
def _add(self, uri): def _add(self, uri):
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@ -109,10 +118,16 @@ class MpdHandler(object):
response = self.handle_request(command, add_ok=False) response = self.handle_request(command, add_ok=False)
if response is not None: if response is not None:
result.append(response) result.append(response)
if response and response[-1].startswith(u'ACK'):
return result
if command_list_ok: if command_list_ok:
response.append(u'list_OK') response.append(u'list_OK')
return result return result
@register(r'^commands$')
def _commands(self):
raise MpdNotImplemented # TODO
@register(r'^consume "(?P<state>[01])"$') @register(r'^consume "(?P<state>[01])"$')
def _consume(self, state): def _consume(self, state):
state = int(state) state = int(state)
@ -133,7 +148,12 @@ class MpdHandler(object):
@register(r'^currentsong$') @register(r'^currentsong$')
def _currentsong(self): def _currentsong(self):
if self.backend.playback.current_track is not None: 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<songpos>\d+)"$') @register(r'^delete "(?P<songpos>\d+)"$')
@register(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$') @register(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
@ -142,12 +162,25 @@ class MpdHandler(object):
@register(r'^deleteid "(?P<songid>\d+)"$') @register(r'^deleteid "(?P<songid>\d+)"$')
def _deleteid(self, songid): 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<outputid>\d+)"$')
def _disableoutput(self, outputid):
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@register(r'^$') @register(r'^$')
def _empty(self): def _empty(self):
pass pass
@register(r'^enableoutput "(?P<outputid>\d+)"$')
def _enableoutput(self, outputid):
raise MpdNotImplemented # TODO
@register(r'^find "(?P<type>(album|artist|title))" "(?P<what>[^"]+)"$') @register(r'^find "(?P<type>(album|artist|title))" "(?P<what>[^"]+)"$')
def _find(self, type, what): def _find(self, type, what):
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@ -220,6 +253,10 @@ class MpdHandler(object):
def _next(self): def _next(self):
return self.backend.playback.next() return self.backend.playback.next()
@register(r'^notcommands$')
def _notcommands(self):
raise MpdNotImplemented # TODO
@register(r'^outputs$') @register(r'^outputs$')
def _outputs(self): def _outputs(self):
return [ return [
@ -511,6 +548,26 @@ class MpdHandler(object):
def _status_xfade(self): def _status_xfade(self):
return 0 # TODO return 0 # TODO
@register(r'^sticker delete "(?P<type>[^"]+)" "(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
def _sticker_delete(self, type, uri, name=None):
raise MpdNotImplemented # TODO
@register(r'^sticker find "(?P<type>[^"]+)" "(?P<uri>[^"]+)" "(?P<name>[^"]+)"$')
def sticker_find(self, type, uri, name):
raise MpdNotImplemented # TODO
@register(r'^sticker get "(?P<type>[^"]+)" "(?P<uri>[^"]+)" "(?P<name>[^"]+)"$')
def _sticker_get(self, type, uri, name):
raise MpdNotImplemented # TODO
@register(r'^sticker list "(?P<type>[^"]+)" "(?P<uri>[^"]+)"$')
def _sticker_list(self, type, uri):
raise MpdNotImplemented # TODO
@register(r'^sticker set "(?P<type>[^"]+)" "(?P<uri>[^"]+)" "(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
def _sticker_set(self, type, uri, name, value):
raise MpdNotImplemented # TODO
@register(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$') @register(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
def _swap(self, songpos1, songpos2): def _swap(self, songpos1, songpos2):
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@ -519,6 +576,10 @@ class MpdHandler(object):
def _swapid(self, songid1, songid2): def _swapid(self, songid1, songid2):
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@register(r'^tagtypes$')
def _tagtypes(self):
raise MpdNotImplemented # TODO
@register(r'^update( "(?P<uri>[^"]+)")*$') @register(r'^update( "(?P<uri>[^"]+)")*$')
def _update(self, uri=None, rescan_unmodified_files=False): def _update(self, uri=None, rescan_unmodified_files=False):
return {'updating_db': 0} # TODO return {'updating_db': 0} # TODO

View File

@ -48,7 +48,8 @@ class MpdSession(asynchat.async_chat):
def handle_request(self, input): def handle_request(self, input):
try: try:
response = self.handler.handle_request(input) response = self.handler.handle_request(input)
self.handle_response(response) if response is not None:
self.handle_response(response)
except MpdAckError, e: except MpdAckError, e:
logger.warning(e) logger.warning(e)
return self.send_response(u'ACK %s' % e) return self.send_response(u'ACK %s' % e)

View File

@ -66,6 +66,12 @@ class CommandListsTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEquals(False, self.h.command_list) 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): def test_command_list_ok_begin(self):
result = self.h.handle_request(u'command_list_ok_begin') result = self.h.handle_request(u'command_list_ok_begin')
self.assert_(result is None) self.assert_(result is None)
@ -94,7 +100,9 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'ACK Not implemented' in result) self.assert_(u'ACK Not implemented' in result)
def test_currentsong(self): 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') result = self.h.handle_request(u'currentsong')
self.assert_(u'file: ' in result) self.assert_(u'file: ' in result)
self.assert_(u'Time: 0' in result) self.assert_(u'Time: 0' in result)
@ -460,7 +468,8 @@ class PlaybackControlHandlerTest(unittest.TestCase):
class CurrentPlaylistHandlerTest(unittest.TestCase): class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.h = handler.MpdHandler(backend=DummyBackend()) self.b = DummyBackend()
self.h = handler.MpdHandler(backend=self.b)
def test_add(self): def test_add(self):
result = self.h.handle_request(u'add "file:///dev/urandom"') 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) self.assert_(u'ACK Not implemented' in result)
def test_deleteid(self): def test_deleteid(self):
self.b.current_playlist.load(Playlist(tracks=[Track(id=0)]))
result = self.h.handle_request(u'deleteid "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): def test_move_songpos(self):
result = self.h.handle_request(u'move "5" "0"') result = self.h.handle_request(u'move "5" "0"')
@ -746,7 +760,35 @@ class StickersHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.h = handler.MpdHandler(backend=DummyBackend()) 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): class ConnectionHandlerTest(unittest.TestCase):
@ -779,6 +821,14 @@ class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.h = handler.MpdHandler(backend=DummyBackend()) 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): def test_outputs(self):
result = self.h.handle_request(u'outputs') result = self.h.handle_request(u'outputs')
self.assert_(u'outputid: 0' in result) self.assert_(u'outputid: 0' in result)
@ -791,10 +841,24 @@ class ReflectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.h = handler.MpdHandler(backend=DummyBackend()) 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): def test_urlhandlers(self):
result = self.h.handle_request(u'urlhandlers') result = self.h.handle_request(u'urlhandlers')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
result = result[0] result = result[0]
self.assert_('dummy:' in result) self.assert_('dummy:' in result)
pass # TODO