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.
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
=============
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

View File

@ -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
<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/
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 <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
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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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<uri>[^"]*)"$')
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<state>[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<songpos>\d+)"$')
@register(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
@ -142,12 +162,25 @@ class MpdHandler(object):
@register(r'^deleteid "(?P<songid>\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<outputid>\d+)"$')
def _disableoutput(self, outputid):
raise MpdNotImplemented # TODO
@register(r'^$')
def _empty(self):
pass
@register(r'^enableoutput "(?P<outputid>\d+)"$')
def _enableoutput(self, outputid):
raise MpdNotImplemented # TODO
@register(r'^find "(?P<type>(album|artist|title))" "(?P<what>[^"]+)"$')
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<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+)"$')
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<uri>[^"]+)")*$')
def _update(self, uri=None, rescan_unmodified_files=False):
return {'updating_db': 0} # TODO

View File

@ -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)

View File

@ -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