Merge branch 'master' of git://github.com/jodal/mopidy
This commit is contained in:
commit
45af7ef21e
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user