diff --git a/.gitignore b/.gitignore
index 3fed7452..21adc7af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.pyc
*.swp
.coverage
+.idea
.noseids
.tox
MANIFEST
diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst
index 062eabdd..7f487881 100644
--- a/docs/api/outputs.rst
+++ b/docs/api/outputs.rst
@@ -13,7 +13,6 @@ Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way.
Output implementations
======================
-* :class:`mopidy.outputs.CustomOutput`
-* :class:`mopidy.outputs.LocalOutput`
-* :class:`mopidy.outputs.NullOutput`
-* :class:`mopidy.outputs.ShoutcastOutput`
+* :class:`mopidy.outputs.custom.CustomOutput`
+* :class:`mopidy.outputs.local.LocalOutput`
+* :class:`mopidy.outputs.shoutcast.ShoutcastOutput`
diff --git a/docs/autodoc_private_members.py b/docs/autodoc_private_members.py
deleted file mode 100644
index 9cb2e49b..00000000
--- a/docs/autodoc_private_members.py
+++ /dev/null
@@ -1,10 +0,0 @@
-def setup(app):
- app.connect('autodoc-skip-member', autodoc_private_members_with_doc)
-
-def autodoc_private_members_with_doc(app, what, name, obj, skip, options):
- if not skip:
- return skip
- if (name.startswith('_') and obj.__doc__ is not None
- and not (name.startswith('__') and name.endswith('__'))):
- return False
- return skip
diff --git a/docs/changes.rst b/docs/changes.rst
index 5e102ece..4831a5e1 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -5,8 +5,8 @@ Changes
This change log is used to track all major changes to Mopidy.
-0.5.0 (in development)
-======================
+v0.5.0 (in development)
+=======================
No description yet.
@@ -24,6 +24,26 @@ No description yet.
- Support passing options to GStreamer. See :option:`--help-gst` for a list of
available options. (Fixes: :issue:`95`)
+- Improve :option:`--list-settings` output. (Fixes: :issue:`91`)
+
+- Replace not decodable characters returned from Spotify instead of throwing an
+ exception, as we won't try to figure out the encoding of non-UTF-8-data.
+
+- MPD frontend:
+
+ - Refactoring and cleanup. Most notably, all request handlers now get an
+ instance of :class:`mopidy.frontends.mpd.dispatcher.MpdContext` as the
+ first argument. The new class contains reference to any object in Mopidy
+ the MPD protocol implementation should need access to.
+
+ - Close the client connection when the command ``close`` is received.
+
+ - Do not allow access to the command ``kill``.
+
+ - ``commands`` and ``notcommands`` now have correct output if password
+ authentication is turned on, but the connected user has not been
+ authenticated yet.
+
- Backends:
- Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
@@ -37,8 +57,8 @@ No description yet.
track without changing the playback state.
-0.4.1 (2011-05-06)
-==================
+v0.4.1 (2011-05-06)
+===================
This is a bug fix release fixing audio problems on older GStreamer and some
minor bugs.
@@ -67,8 +87,8 @@ minor bugs.
configured. (Fixes: :issue:`84`)
-0.4.0 (2011-04-27)
-==================
+v0.4.0 (2011-04-27)
+===================
Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've
fixed a bunch of issues and bugs, with the help of several new contributors
@@ -166,8 +186,8 @@ loading from Mopidy 0.3.0 is still present.
the debug log, to ease debugging of issues with attached debug logs.
-0.3.1 (2010-01-22)
-==================
+v0.3.1 (2010-01-22)
+===================
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
@@ -180,8 +200,8 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
installed if the installation is executed as the root user.
-0.3.0 (2010-01-22)
-==================
+v0.3.0 (2010-01-22)
+===================
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large
changes. The main features are support for high bitrate audio from Spotify, and
@@ -335,8 +355,8 @@ to this problem.
:class:`mopidy.outputs.base.BaseOutput`.
-0.2.1 (2011-01-07)
-==================
+v0.2.1 (2011-01-07)
+===================
This is a maintenance release without any new features.
@@ -348,8 +368,8 @@ This is a maintenance release without any new features.
failure.
-0.2.0 (2010-10-24)
-==================
+v0.2.0 (2010-10-24)
+===================
In Mopidy 0.2.0 we've added a `Last.fm `_ scrobbling
support, which means that Mopidy now can submit meta data about the tracks you
@@ -416,8 +436,8 @@ searching at the same time, thanks to Valentin David.
should now exit immediately.
-0.1.0 (2010-08-23)
-==================
+v0.1.0 (2010-08-23)
+===================
After three weeks of long nights and sprints we're finally pleased enough with
the state of Mopidy to remove the alpha label, and do a regular release.
@@ -548,8 +568,8 @@ fixing the OS X issues for a future release. You can track the progress at
:meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead.
-0.1.0a3 (2010-08-03)
-====================
+v0.1.0a3 (2010-08-03)
+=====================
In the last two months, Mopidy's MPD frontend has gotten lots of stability
fixes and error handling improvements, proper support for having the same track
@@ -626,8 +646,8 @@ Enjoy the best alpha relase of Mopidy ever :-)
``cp_track``.
-0.1.0a2 (2010-06-02)
-====================
+v0.1.0a2 (2010-06-02)
+=====================
It has been a rather slow month for Mopidy, but we would like to keep up with
the established pace of at least a release per month.
@@ -642,8 +662,8 @@ the established pace of at least a release per month.
control :class:`mopidy.mixers.alsa.AlsaMixer` should use.
-0.1.0a1 (2010-05-04)
-====================
+v0.1.0a1 (2010-05-04)
+=====================
Since the previous release Mopidy has seen about 300 commits, more than 200 new
tests, a libspotify release, and major feature additions to Spotify. The new
@@ -683,8 +703,8 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
- And much more.
-0.1.0a0 (2010-03-27)
-====================
+v0.1.0a0 (2010-03-27)
+=====================
"*Release early. Release often. Listen to your customers.*" wrote Eric S.
Raymond in *The Cathedral and the Bazaar*.
diff --git a/docs/conf.py b/docs/conf.py
index 7ae3c126..aeada340 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,7 @@ import mopidy
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'autodoc_private_members',
+extensions = ['sphinx.ext.autodoc',
'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
diff --git a/docs/index.rst b/docs/index.rst
index 0af45835..769aed20 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -33,13 +33,13 @@ User documentation
.. toctree::
:maxdepth: 3
- changes
installation/index
settings
running
clients/index
authors
licenses
+ changes
Reference documentation
=======================
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index d1fbd0f6..1f497e3a 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -98,7 +98,7 @@ install Mopidy from PyPI using Pip.
#. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo brew install pip # On OS X
+ sudo easy_install pip # On OS X
#. To install the currently latest stable release of Mopidy::
@@ -132,7 +132,7 @@ Mopidy's ``develop`` branch.
#. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo brew install pip # On OS X
+ sudo easy_install pip # On OS X
#. To install the latest snapshot of Mopidy, run::
@@ -155,7 +155,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
#. Then install Git, if haven't already::
sudo aptitude install git-core # On Ubuntu/Debian
- sudo brew install git # On OS X
+ sudo brew install git # On OS X using Homebrew
#. Clone the official Mopidy repository, or your own fork of it::
diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst
index 35128e70..6120c2a6 100644
--- a/docs/modules/frontends/mpd.rst
+++ b/docs/modules/frontends/mpd.rst
@@ -2,6 +2,8 @@
:mod:`mopidy.frontends.mpd` -- MPD server
*****************************************
+.. inheritance-diagram:: mopidy.frontends.mpd
+
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend
:members:
@@ -11,28 +13,30 @@
MPD server
==========
+.. inheritance-diagram:: mopidy.frontends.mpd.server
+
.. automodule:: mopidy.frontends.mpd.server
:synopsis: MPD server
:members:
:undoc-members:
-.. inheritance-diagram:: mopidy.frontends.mpd.server
-
MPD session
===========
+.. inheritance-diagram:: mopidy.frontends.mpd.session
+
.. automodule:: mopidy.frontends.mpd.session
:synopsis: MPD client session
:members:
:undoc-members:
-.. inheritance-diagram:: mopidy.frontends.mpd.session
-
MPD dispatcher
==============
+.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
+
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
:members:
diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst
index 7da29fbc..37ff0390 100644
--- a/docs/modules/outputs.rst
+++ b/docs/modules/outputs.rst
@@ -6,10 +6,8 @@ The following GStreamer audio outputs implements the :ref:`output-api`.
.. inheritance-diagram:: mopidy.outputs
-.. autoclass:: mopidy.outputs.CustomOutput
+.. autoclass:: mopidy.outputs.custom.CustomOutput
-.. autoclass:: mopidy.outputs.LocalOutput
+.. autoclass:: mopidy.outputs.local.LocalOutput
-.. autoclass:: mopidy.outputs.NullOutput
-
-.. autoclass:: mopidy.outputs.ShoutcastOutput
+.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index ffdce176..2633f166 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -2,6 +2,8 @@ from copy import copy
import logging
import random
+from mopidy.models import CpTrack
+
logger = logging.getLogger('mopidy.backends.base')
class CurrentPlaylistController(object):
@@ -66,7 +68,7 @@ class CurrentPlaylistController(object):
"""
assert at_position <= len(self._cp_tracks), \
u'at_position can not be greater than playlist length'
- cp_track = (self.version, track)
+ cp_track = CpTrack(self.version, track)
if at_position is not None:
self._cp_tracks.insert(at_position, cp_track)
else:
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index 42070a40..eeb4b2f3 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -80,12 +80,12 @@ class PlaybackController(object):
def _get_cpid(self, cp_track):
if cp_track is None:
return None
- return cp_track[0]
+ return cp_track.cpid
def _get_track(self, cp_track):
if cp_track is None:
return None
- return cp_track[1]
+ return cp_track.track
@property
def current_cpid(self):
@@ -350,7 +350,7 @@ class PlaybackController(object):
self.stop(clear_current_track=True)
if self.consume:
- self.backend.current_playlist.remove(cpid=original_cp_track[0])
+ self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
def on_current_playlist_change(self):
"""
@@ -410,7 +410,7 @@ class PlaybackController(object):
self.stop()
self.current_cp_track = cp_track
self.state = self.PLAYING
- if not self.provider.play(cp_track[1]):
+ if not self.provider.play(cp_track.track):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_track)
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index 09064db2..f34283c6 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -106,7 +106,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate,
'channels': channels,
}
- self.gstreamer.deliver_data(capabilites, bytes(frames))
+ self.gstreamer.emit_data(capabilites, bytes(frames))
def play_token_lost(self, session):
"""Callback used by pyspotify"""
@@ -120,7 +120,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug(u'End of data stream reached')
- self.gstreamer.end_of_data_stream()
+ self.gstreamer.emit_end_of_stream()
def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data
diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py
index dca2b285..21abdf78 100644
--- a/mopidy/backends/spotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -16,28 +16,29 @@ class SpotifyTranslator(object):
return Artist(name=u'[loading...]')
return Artist(
uri=str(Link.from_artist(spotify_artist)),
- name=spotify_artist.name().decode(ENCODING),
+ name=spotify_artist.name().decode(ENCODING, 'replace'),
)
@classmethod
def to_mopidy_album(cls, spotify_album):
- if not spotify_album.is_loaded():
+ if spotify_album is None or not spotify_album.is_loaded():
return Album(name=u'[loading...]')
# TODO pyspotify got much more data on albums than this
- return Album(name=spotify_album.name().decode(ENCODING))
+ return Album(name=spotify_album.name().decode(ENCODING, 'replace'))
@classmethod
def to_mopidy_track(cls, spotify_track):
uri = str(Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]')
- if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
+ if (spotify_track.album() is not None and
+ dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR):
date = dt.date(spotify_track.album().year(), 1, 1)
else:
date = None
return Track(
uri=uri,
- name=spotify_track.name().decode(ENCODING),
+ name=spotify_track.name().decode(ENCODING, 'replace'),
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
album=cls.to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(),
@@ -56,7 +57,7 @@ class SpotifyTranslator(object):
try:
return Playlist(
uri=str(Link.from_playlist(spotify_playlist)),
- name=spotify_playlist.name().decode(ENCODING),
+ name=spotify_playlist.name().decode(ENCODING, 'replace'),
# FIXME if check on link is a hackish workaround for is_local
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist
if str(Link.from_track(t, 0))],
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index f5c30b23..91cdc5e7 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -1,10 +1,12 @@
+import logging
import re
+from pykka import ActorDeadError
from pykka.registry import ActorRegistry
+from mopidy import settings
from mopidy.backends.base import Backend
-from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
- MpdUnknownCommand)
+from mopidy.frontends.mpd import exceptions
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
# Do not remove the following import. The protocol modules must be imported to
# get them registered as request handlers.
@@ -16,6 +18,8 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list,
from mopidy.mixers.base import BaseMixer
from mopidy.utils import flatten
+logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
+
class MpdDispatcher(object):
"""
The MPD session feeds the MPD dispatcher with requests. The dispatcher
@@ -23,67 +27,186 @@ class MpdDispatcher(object):
back to the MPD session.
"""
- # XXX Consider merging MpdDispatcher into MpdSession
-
- def __init__(self):
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) == 1, 'Expected exactly one running backend.'
- self.backend = backend_refs[0].proxy()
-
- mixer_refs = ActorRegistry.get_by_class(BaseMixer)
- assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
- self.mixer = mixer_refs[0].proxy()
-
+ def __init__(self, session=None):
+ self.authenticated = False
self.command_list = False
self.command_list_ok = False
+ self.command_list_index = None
+ self.context = MpdContext(self, session=session)
- def handle_request(self, request, command_list_index=None):
+ def handle_request(self, request, current_command_list_index=None):
"""Dispatch incoming requests to the correct handler."""
- if self.command_list is not False and request != u'command_list_end':
- self.command_list.append(request)
- return None
- try:
- (handler, kwargs) = self.find_handler(request)
- result = handler(self, **kwargs)
- except MpdAckError as e:
- if command_list_index is not None:
- e.index = command_list_index
- return self.handle_response(e.get_mpd_ack(), add_ok=False)
- if request in (u'command_list_begin', u'command_list_ok_begin'):
- return None
- if command_list_index is not None:
- return self.handle_response(result, add_ok=False)
- return self.handle_response(result)
+ self.command_list_index = current_command_list_index
+ response = []
+ filter_chain = [
+ self._catch_mpd_ack_errors_filter,
+ self._authenticate_filter,
+ self._command_list_filter,
+ self._add_ok_filter,
+ self._call_handler_filter,
+ ]
+ return self._call_next_filter(request, response, filter_chain)
- def find_handler(self, request):
- """Find the correct handler for a request."""
+ def _call_next_filter(self, request, response, filter_chain):
+ if filter_chain:
+ next_filter = filter_chain.pop(0)
+ return next_filter(request, response, filter_chain)
+ else:
+ return response
+
+
+ ### Filter: catch MPD ACK errors
+
+ def _catch_mpd_ack_errors_filter(self, request, response, filter_chain):
+ try:
+ return self._call_next_filter(request, response, filter_chain)
+ except exceptions.MpdAckError as mpd_ack_error:
+ if self.command_list_index is not None:
+ mpd_ack_error.index = self.command_list_index
+ return [mpd_ack_error.get_mpd_ack()]
+
+
+ ### Filter: authenticate
+
+ def _authenticate_filter(self, request, response, filter_chain):
+ if self.authenticated:
+ return self._call_next_filter(request, response, filter_chain)
+ elif settings.MPD_SERVER_PASSWORD is None:
+ self.authenticated = True
+ return self._call_next_filter(request, response, filter_chain)
+ else:
+ command_name = request.split(' ')[0]
+ command_names_not_requiring_auth = [
+ command.name for command in mpd_commands
+ if not command.auth_required]
+ if command_name in command_names_not_requiring_auth:
+ return self._call_next_filter(request, response, filter_chain)
+ else:
+ raise exceptions.MpdPermissionError(command=command_name)
+
+
+ ### Filter: command list
+
+ def _command_list_filter(self, request, response, filter_chain):
+ if self._is_receiving_command_list(request):
+ self.command_list.append(request)
+ return []
+ else:
+ response = self._call_next_filter(request, response, filter_chain)
+ if (self._is_receiving_command_list(request) or
+ self._is_processing_command_list(request)):
+ if response and response[-1] == u'OK':
+ response = response[:-1]
+ return response
+
+ def _is_receiving_command_list(self, request):
+ return (self.command_list is not False
+ and request != u'command_list_end')
+
+ def _is_processing_command_list(self, request):
+ return (self.command_list_index is not None
+ and request != u'command_list_end')
+
+
+ ### Filter: add OK
+
+ def _add_ok_filter(self, request, response, filter_chain):
+ response = self._call_next_filter(request, response, filter_chain)
+ if not self._has_error(response):
+ response.append(u'OK')
+ return response
+
+ def _has_error(self, response):
+ return response and response[-1].startswith(u'ACK')
+
+
+ ### Filter: call handler
+
+ def _call_handler_filter(self, request, response, filter_chain):
+ try:
+ response = self._format_response(self._call_handler(request))
+ return self._call_next_filter(request, response, filter_chain)
+ except ActorDeadError as e:
+ logger.warning(u'Tried to communicate with dead actor.')
+ raise exceptions.MpdSystemError(e.message)
+
+ def _call_handler(self, request):
+ (handler, kwargs) = self._find_handler(request)
+ return handler(self.context, **kwargs)
+
+ def _find_handler(self, request):
for pattern in request_handlers:
matches = re.match(pattern, request)
if matches is not None:
return (request_handlers[pattern], matches.groupdict())
- command = request.split(' ')[0]
- if command in mpd_commands:
- raise MpdArgError(u'incorrect arguments', command=command)
- raise MpdUnknownCommand(command=command)
+ command_name = request.split(' ')[0]
+ if command_name in [command.name for command in mpd_commands]:
+ raise exceptions.MpdArgError(u'incorrect arguments',
+ command=command_name)
+ raise exceptions.MpdUnknownCommand(command=command_name)
- def handle_response(self, result, add_ok=True):
- """Format the response from a request handler."""
- response = []
+ def _format_response(self, response):
+ formatted_response = []
+ for element in self._listify_result(response):
+ formatted_response.extend(self._format_lines(element))
+ return formatted_response
+
+ def _listify_result(self, result):
if result is None:
- result = []
- elif isinstance(result, set):
- result = list(result)
- elif not isinstance(result, list):
- result = [result]
- for line in flatten(result):
- if isinstance(line, dict):
- for (key, value) in line.items():
- response.append(u'%s: %s' % (key, value))
- elif isinstance(line, tuple):
- (key, value) = line
- response.append(u'%s: %s' % (key, value))
- else:
- response.append(line)
- if add_ok and (not response or not response[-1].startswith(u'ACK')):
- response.append(u'OK')
- return response
+ return []
+ if isinstance(result, set):
+ return flatten(list(result))
+ if not isinstance(result, list):
+ return [result]
+ return flatten(result)
+
+ def _format_lines(self, line):
+ if isinstance(line, dict):
+ return [u'%s: %s' % (key, value) for (key, value) in line.items()]
+ if isinstance(line, tuple):
+ (key, value) = line
+ return [u'%s: %s' % (key, value)]
+ return [line]
+
+
+class MpdContext(object):
+ """
+ This object is passed as the first argument to all MPD command handlers to
+ give the command handlers access to important parts of Mopidy.
+ """
+
+ #: The current :class:`MpdDispatcher`.
+ dispatcher = None
+
+ #: The current :class:`mopidy.frontends.mpd.session.MpdSession`.
+ session = None
+
+ def __init__(self, dispatcher, session=None):
+ self.dispatcher = dispatcher
+ self.session = session
+ self._backend = None
+ self._mixer = None
+
+ @property
+ def backend(self):
+ """
+ The backend. An instance of :class:`mopidy.backends.base.Backend`.
+ """
+ if self._backend is not None:
+ return self._backend
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, 'Expected exactly one running backend.'
+ self._backend = backend_refs[0].proxy()
+ return self._backend
+
+ @property
+ def mixer(self):
+ """
+ The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
+ """
+ if self._mixer is not None:
+ return self._mixer
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self._mixer = mixer_refs[0].proxy()
+ return self._mixer
diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py
index faf4ce2f..661d6905 100644
--- a/mopidy/frontends/mpd/exceptions.py
+++ b/mopidy/frontends/mpd/exceptions.py
@@ -16,10 +16,11 @@ class MpdAckError(MopidyException):
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
- def __init__(self, message=u'', error_code=0, index=0, command=u''):
- super(MpdAckError, self).__init__(message, error_code, index, command)
+ error_code = 0
+
+ def __init__(self, message=u'', index=0, command=u''):
+ super(MpdAckError, self).__init__(message, index, command)
self.message = message
- self.error_code = error_code
self.index = index
self.command = command
@@ -30,31 +31,38 @@ class MpdAckError(MopidyException):
ACK [%(error_code)i@%(index)i] {%(command)s} description
"""
return u'ACK [%i@%i] {%s} %s' % (
- self.error_code, self.index, self.command, self.message)
+ self.__class__.error_code, self.index, self.command, self.message)
class MpdArgError(MpdAckError):
- def __init__(self, *args, **kwargs):
- super(MpdArgError, self).__init__(*args, **kwargs)
- self.error_code = MpdAckError.ACK_ERROR_ARG
+ error_code = MpdAckError.ACK_ERROR_ARG
class MpdPasswordError(MpdAckError):
+ error_code = MpdAckError.ACK_ERROR_PASSWORD
+
+class MpdPermissionError(MpdAckError):
+ error_code = MpdAckError.ACK_ERROR_PERMISSION
+
def __init__(self, *args, **kwargs):
- super(MpdPasswordError, self).__init__(*args, **kwargs)
- self.error_code = MpdAckError.ACK_ERROR_PASSWORD
+ super(MpdPermissionError, self).__init__(*args, **kwargs)
+ self.message = u'you don\'t have permission for "%s"' % self.command
class MpdUnknownCommand(MpdAckError):
+ error_code = MpdAckError.ACK_ERROR_UNKNOWN
+
def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
self.message = u'unknown command "%s"' % self.command
self.command = u''
- self.error_code = MpdAckError.ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError):
- def __init__(self, *args, **kwargs):
- super(MpdNoExistError, self).__init__(*args, **kwargs)
- self.error_code = MpdAckError.ACK_ERROR_NO_EXIST
+ error_code = MpdAckError.ACK_ERROR_NO_EXIST
+
+class MpdSystemError(MpdAckError):
+ error_code = MpdAckError.ACK_ERROR_SYSTEM
class MpdNotImplemented(MpdAckError):
+ error_code = 0
+
def __init__(self, *args, **kwargs):
super(MpdNotImplemented, self).__init__(*args, **kwargs)
self.message = u'Not implemented'
diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py
index 6689f627..f0b56a57 100644
--- a/mopidy/frontends/mpd/protocol/__init__.py
+++ b/mopidy/frontends/mpd/protocol/__init__.py
@@ -10,6 +10,7 @@ implement our own MPD server which is compatible with the numerous existing
`MPD clients `_.
"""
+from collections import namedtuple
import re
#: The MPD protocol uses UTF-8 for encoding all data.
@@ -21,12 +22,16 @@ LINE_TERMINATOR = u'\n'
#: The MPD protocol version is 0.16.0.
VERSION = u'0.16.0'
+MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required'])
+
+#: List of all available commands, represented as :class:`MpdCommand` objects.
mpd_commands = set()
+
request_handlers = {}
-def handle_pattern(pattern):
+def handle_request(pattern, auth_required=True):
"""
- Decorator for connecting command handlers to command patterns.
+ Decorator for connecting command handlers to command requests.
If you use named groups in the pattern, the decorated method will get the
groups as keyword arguments. If the group is optional, remember to give the
@@ -35,7 +40,7 @@ def handle_pattern(pattern):
For example, if the command is ``do that thing`` the ``what`` argument will
be ``this thing``::
- @handle_pattern('^do (?P.+)$')
+ @handle_request('^do (?P.+)$')
def do(what):
...
@@ -45,7 +50,8 @@ def handle_pattern(pattern):
def decorator(func):
match = re.search('([a-z_]+)', pattern)
if match is not None:
- mpd_commands.add(match.group())
+ mpd_commands.add(
+ MpdCommand(name=match.group(), auth_required=auth_required))
if pattern in request_handlers:
raise ValueError(u'Tried to redefine handler for %s with %s' % (
pattern, func))
diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py
index 98c1d645..7147963a 100644
--- a/mopidy/frontends/mpd/protocol/audio_output.py
+++ b/mopidy/frontends/mpd/protocol/audio_output.py
@@ -1,8 +1,8 @@
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
-@handle_pattern(r'^disableoutput "(?P\d+)"$')
-def disableoutput(frontend, outputid):
+@handle_request(r'^disableoutput "(?P\d+)"$')
+def disableoutput(context, outputid):
"""
*musicpd.org, audio output section:*
@@ -12,8 +12,8 @@ def disableoutput(frontend, outputid):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^enableoutput "(?P\d+)"$')
-def enableoutput(frontend, outputid):
+@handle_request(r'^enableoutput "(?P\d+)"$')
+def enableoutput(context, outputid):
"""
*musicpd.org, audio output section:*
@@ -23,8 +23,8 @@ def enableoutput(frontend, outputid):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^outputs$')
-def outputs(frontend):
+@handle_request(r'^outputs$')
+def outputs(context):
"""
*musicpd.org, audio output section:*
diff --git a/mopidy/frontends/mpd/protocol/command_list.py b/mopidy/frontends/mpd/protocol/command_list.py
index b3df0be6..37e5c93d 100644
--- a/mopidy/frontends/mpd/protocol/command_list.py
+++ b/mopidy/frontends/mpd/protocol/command_list.py
@@ -1,8 +1,8 @@
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
-@handle_pattern(r'^command_list_begin$')
-def command_list_begin(frontend):
+@handle_request(r'^command_list_begin$')
+def command_list_begin(context):
"""
*musicpd.org, command list section:*
@@ -18,31 +18,33 @@ def command_list_begin(frontend):
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
returned for each successful command executed in the command list.
"""
- frontend.command_list = []
- frontend.command_list_ok = False
+ context.dispatcher.command_list = []
+ context.dispatcher.command_list_ok = False
-@handle_pattern(r'^command_list_end$')
-def command_list_end(frontend):
+@handle_request(r'^command_list_end$')
+def command_list_end(context):
"""See :meth:`command_list_begin()`."""
- if frontend.command_list is False:
+ if context.dispatcher.command_list is False:
# Test for False exactly, and not e.g. empty list
raise MpdUnknownCommand(command='command_list_end')
- (command_list, frontend.command_list) = (frontend.command_list, False)
- (command_list_ok, frontend.command_list_ok) = (
- frontend.command_list_ok, False)
- result = []
- for i, command in enumerate(command_list):
- response = frontend.handle_request(command, command_list_index=i)
- if response is not None:
- result.append(response)
- if response and response[-1].startswith(u'ACK'):
- return result
+ (command_list, context.dispatcher.command_list) = (
+ context.dispatcher.command_list, False)
+ (command_list_ok, context.dispatcher.command_list_ok) = (
+ context.dispatcher.command_list_ok, False)
+ command_list_response = []
+ for index, command in enumerate(command_list):
+ response = context.dispatcher.handle_request(
+ command, current_command_list_index=index)
+ command_list_response.extend(response)
+ if (command_list_response and
+ command_list_response[-1].startswith(u'ACK')):
+ return command_list_response
if command_list_ok:
- response.append(u'list_OK')
- return result
+ command_list_response.append(u'list_OK')
+ return command_list_response
-@handle_pattern(r'^command_list_ok_begin$')
-def command_list_ok_begin(frontend):
+@handle_request(r'^command_list_ok_begin$')
+def command_list_ok_begin(context):
"""See :meth:`command_list_begin()`."""
- frontend.command_list = []
- frontend.command_list_ok = True
+ context.dispatcher.command_list = []
+ context.dispatcher.command_list_ok = True
diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py
index 65811d09..ff230173 100644
--- a/mopidy/frontends/mpd/protocol/connection.py
+++ b/mopidy/frontends/mpd/protocol/connection.py
@@ -1,9 +1,10 @@
from mopidy import settings
-from mopidy.frontends.mpd.protocol import handle_pattern
-from mopidy.frontends.mpd.exceptions import MpdPasswordError
+from mopidy.frontends.mpd.protocol import handle_request
+from mopidy.frontends.mpd.exceptions import (MpdPasswordError,
+ MpdPermissionError)
-@handle_pattern(r'^close$')
-def close(frontend):
+@handle_request(r'^close$', auth_required=False)
+def close(context):
"""
*musicpd.org, connection section:*
@@ -11,10 +12,10 @@ def close(frontend):
Closes the connection to MPD.
"""
- pass # TODO
+ context.session.close()
-@handle_pattern(r'^kill$')
-def kill(frontend):
+@handle_request(r'^kill$')
+def kill(context):
"""
*musicpd.org, connection section:*
@@ -22,10 +23,10 @@ def kill(frontend):
Kills MPD.
"""
- pass # TODO
+ raise MpdPermissionError(command=u'kill')
-@handle_pattern(r'^password "(?P[^"]+)"$')
-def password_(frontend, password):
+@handle_request(r'^password "(?P[^"]+)"$', auth_required=False)
+def password_(context, password):
"""
*musicpd.org, connection section:*
@@ -34,14 +35,13 @@ def password_(frontend, password):
This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password.
"""
- # You will not get to this code without being authenticated. This is for
- # when you are already authenticated, and are sending additional 'password'
- # requests.
- if settings.MPD_SERVER_PASSWORD != password:
+ if password == settings.MPD_SERVER_PASSWORD:
+ context.dispatcher.authenticated = True
+ else:
raise MpdPasswordError(u'incorrect password', command=u'password')
-@handle_pattern(r'^ping$')
-def ping(frontend):
+@handle_request(r'^ping$', auth_required=False)
+def ping(context):
"""
*musicpd.org, connection section:*
diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py
index 8ef5e026..8e26013d 100644
--- a/mopidy/frontends/mpd/protocol/current_playlist.py
+++ b/mopidy/frontends/mpd/protocol/current_playlist.py
@@ -1,10 +1,10 @@
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.translator import tracks_to_mpd_format
-@handle_pattern(r'^add "(?P[^"]*)"$')
-def add(frontend, uri):
+@handle_request(r'^add "(?P[^"]*)"$')
+def add(context, uri):
"""
*musicpd.org, current playlist section:*
@@ -19,17 +19,17 @@ def add(frontend, uri):
"""
if not uri:
return
- for handler_prefix in frontend.backend.uri_handlers.get():
+ for handler_prefix in context.backend.uri_handlers.get():
if uri.startswith(handler_prefix):
- track = frontend.backend.library.lookup(uri).get()
+ track = context.backend.library.lookup(uri).get()
if track is not None:
- frontend.backend.current_playlist.add(track)
+ context.backend.current_playlist.add(track)
return
raise MpdNoExistError(
u'directory or file not found', command=u'add')
-@handle_pattern(r'^addid "(?P[^"]*)"( "(?P\d+)")*$')
-def addid(frontend, uri, songpos=None):
+@handle_request(r'^addid "(?P[^"]*)"( "(?P\d+)")*$')
+def addid(context, uri, songpos=None):
"""
*musicpd.org, current playlist section:*
@@ -51,18 +51,18 @@ def addid(frontend, uri, songpos=None):
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos is not None:
songpos = int(songpos)
- track = frontend.backend.library.lookup(uri).get()
+ track = context.backend.library.lookup(uri).get()
if track is None:
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos and songpos > len(
- frontend.backend.current_playlist.tracks.get()):
+ context.backend.current_playlist.tracks.get()):
raise MpdArgError(u'Bad song index', command=u'addid')
- cp_track = frontend.backend.current_playlist.add(track,
+ cp_track = context.backend.current_playlist.add(track,
at_position=songpos).get()
- return ('Id', cp_track[0])
+ return ('Id', cp_track.cpid)
-@handle_pattern(r'^delete "(?P\d+):(?P\d+)*"$')
-def delete_range(frontend, start, end=None):
+@handle_request(r'^delete "(?P\d+):(?P\d+)*"$')
+def delete_range(context, start, end=None):
"""
*musicpd.org, current playlist section:*
@@ -74,25 +74,25 @@ def delete_range(frontend, start, end=None):
if end is not None:
end = int(end)
else:
- end = len(frontend.backend.current_playlist.tracks.get())
- cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end]
+ end = len(context.backend.current_playlist.tracks.get())
+ cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end]
if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks:
- frontend.backend.current_playlist.remove(cpid=cpid)
+ context.backend.current_playlist.remove(cpid=cpid)
-@handle_pattern(r'^delete "(?P\d+)"$')
-def delete_songpos(frontend, songpos):
+@handle_request(r'^delete "(?P\d+)"$')
+def delete_songpos(context, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
- (cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos]
- frontend.backend.current_playlist.remove(cpid=cpid)
+ (cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos]
+ context.backend.current_playlist.remove(cpid=cpid)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete')
-@handle_pattern(r'^deleteid "(?P\d+)"$')
-def deleteid(frontend, cpid):
+@handle_request(r'^deleteid "(?P\d+)"$')
+def deleteid(context, cpid):
"""
*musicpd.org, current playlist section:*
@@ -102,14 +102,14 @@ def deleteid(frontend, cpid):
"""
try:
cpid = int(cpid)
- if frontend.backend.playback.current_cpid.get() == cpid:
- frontend.backend.playback.next()
- return frontend.backend.current_playlist.remove(cpid=cpid).get()
+ if context.backend.playback.current_cpid.get() == cpid:
+ context.backend.playback.next()
+ return context.backend.current_playlist.remove(cpid=cpid).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'deleteid')
-@handle_pattern(r'^clear$')
-def clear(frontend):
+@handle_request(r'^clear$')
+def clear(context):
"""
*musicpd.org, current playlist section:*
@@ -117,10 +117,10 @@ def clear(frontend):
Clears the current playlist.
"""
- frontend.backend.current_playlist.clear()
+ context.backend.current_playlist.clear()
-@handle_pattern(r'^move "(?P\d+):(?P\d+)*" "(?P\d+)"$')
-def move_range(frontend, start, to, end=None):
+@handle_request(r'^move "(?P\d+):(?P\d+)*" "(?P\d+)"$')
+def move_range(context, start, to, end=None):
"""
*musicpd.org, current playlist section:*
@@ -130,21 +130,21 @@ def move_range(frontend, start, to, end=None):
``TO`` in the playlist.
"""
if end is None:
- end = len(frontend.backend.current_playlist.tracks.get())
+ end = len(context.backend.current_playlist.tracks.get())
start = int(start)
end = int(end)
to = int(to)
- frontend.backend.current_playlist.move(start, end, to)
+ context.backend.current_playlist.move(start, end, to)
-@handle_pattern(r'^move "(?P\d+)" "(?P\d+)"$')
-def move_songpos(frontend, songpos, to):
+@handle_request(r'^move "(?P\d+)" "(?P\d+)"$')
+def move_songpos(context, songpos, to):
"""See :meth:`move_range`."""
songpos = int(songpos)
to = int(to)
- frontend.backend.current_playlist.move(songpos, songpos + 1, to)
+ context.backend.current_playlist.move(songpos, songpos + 1, to)
-@handle_pattern(r'^moveid "(?P\d+)" "(?P\d+)"$')
-def moveid(frontend, cpid, to):
+@handle_request(r'^moveid "(?P\d+)" "(?P\d+)"$')
+def moveid(context, cpid, to):
"""
*musicpd.org, current playlist section:*
@@ -156,13 +156,13 @@ def moveid(frontend, cpid, to):
"""
cpid = int(cpid)
to = int(to)
- cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
- position = frontend.backend.current_playlist.cp_tracks.get().index(
+ cp_track = context.backend.current_playlist.get(cpid=cpid).get()
+ position = context.backend.current_playlist.cp_tracks.get().index(
cp_track)
- frontend.backend.current_playlist.move(position, position + 1, to)
+ context.backend.current_playlist.move(position, position + 1, to)
-@handle_pattern(r'^playlist$')
-def playlist(frontend):
+@handle_request(r'^playlist$')
+def playlist(context):
"""
*musicpd.org, current playlist section:*
@@ -174,11 +174,11 @@ def playlist(frontend):
Do not use this, instead use ``playlistinfo``.
"""
- return playlistinfo(frontend)
+ return playlistinfo(context)
-@handle_pattern(r'^playlistfind (?P[^"]+) "(?P[^"]+)"$')
-@handle_pattern(r'^playlistfind "(?P[^"]+)" "(?P[^"]+)"$')
-def playlistfind(frontend, tag, needle):
+@handle_request(r'^playlistfind (?P[^"]+) "(?P[^"]+)"$')
+@handle_request(r'^playlistfind "(?P[^"]+)" "(?P[^"]+)"$')
+def playlistfind(context, tag, needle):
"""
*musicpd.org, current playlist section:*
@@ -192,17 +192,17 @@ def playlistfind(frontend, tag, needle):
"""
if tag == 'filename':
try:
- cp_track = frontend.backend.current_playlist.get(uri=needle).get()
+ cp_track = context.backend.current_playlist.get(uri=needle).get()
(cpid, track) = cp_track
- position = frontend.backend.current_playlist.cp_tracks.get().index(
+ position = context.backend.current_playlist.cp_tracks.get().index(
cp_track)
return track.mpd_format(cpid=cpid, position=position)
except LookupError:
return None
raise MpdNotImplemented # TODO
-@handle_pattern(r'^playlistid( "(?P\d+)")*$')
-def playlistid(frontend, cpid=None):
+@handle_request(r'^playlistid( "(?P\d+)")*$')
+def playlistid(context, cpid=None):
"""
*musicpd.org, current playlist section:*
@@ -214,22 +214,22 @@ def playlistid(frontend, cpid=None):
if cpid is not None:
try:
cpid = int(cpid)
- cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
- position = frontend.backend.current_playlist.cp_tracks.get().index(
+ cp_track = context.backend.current_playlist.get(cpid=cpid).get()
+ position = context.backend.current_playlist.cp_tracks.get().index(
cp_track)
- return cp_track[1].mpd_format(position=position, cpid=cpid)
+ return cp_track.track.mpd_format(position=position, cpid=cpid)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid')
else:
cpids = [ct[0] for ct in
- frontend.backend.current_playlist.cp_tracks.get()]
+ context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- frontend.backend.current_playlist.tracks.get(), cpids=cpids)
+ context.backend.current_playlist.tracks.get(), cpids=cpids)
-@handle_pattern(r'^playlistinfo$')
-@handle_pattern(r'^playlistinfo "(?P-?\d+)"$')
-@handle_pattern(r'^playlistinfo "(?P\d+):(?P\d+)*"$')
-def playlistinfo(frontend, songpos=None,
+@handle_request(r'^playlistinfo$')
+@handle_request(r'^playlistinfo "(?P-?\d+)"$')
+@handle_request(r'^playlistinfo "(?P\d+):(?P\d+)*"$')
+def playlistinfo(context, songpos=None,
start=None, end=None):
"""
*musicpd.org, current playlist section:*
@@ -255,30 +255,30 @@ def playlistinfo(frontend, songpos=None,
if start == -1:
end = None
cpids = [ct[0] for ct in
- frontend.backend.current_playlist.cp_tracks.get()]
+ context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- frontend.backend.current_playlist.tracks.get(),
+ context.backend.current_playlist.tracks.get(),
start, end, cpids=cpids)
else:
if start is None:
start = 0
start = int(start)
if not (0 <= start <= len(
- frontend.backend.current_playlist.tracks.get())):
+ context.backend.current_playlist.tracks.get())):
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None:
end = int(end)
- if end > len(frontend.backend.current_playlist.tracks.get()):
+ if end > len(context.backend.current_playlist.tracks.get()):
end = None
cpids = [ct[0] for ct in
- frontend.backend.current_playlist.cp_tracks.get()]
+ context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- frontend.backend.current_playlist.tracks.get(),
+ context.backend.current_playlist.tracks.get(),
start, end, cpids=cpids)
-@handle_pattern(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$')
-@handle_pattern(r'^playlistsearch (?P\S+) "(?P[^"]+)"$')
-def playlistsearch(frontend, tag, needle):
+@handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$')
+@handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$')
+def playlistsearch(context, tag, needle):
"""
*musicpd.org, current playlist section:*
@@ -294,9 +294,9 @@ def playlistsearch(frontend, tag, needle):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^plchanges (?P-?\d+)$')
-@handle_pattern(r'^plchanges "(?P-?\d+)"$')
-def plchanges(frontend, version):
+@handle_request(r'^plchanges (?P-?\d+)$')
+@handle_request(r'^plchanges "(?P-?\d+)"$')
+def plchanges(context, version):
"""
*musicpd.org, current playlist section:*
@@ -312,14 +312,14 @@ def plchanges(frontend, version):
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
"""
# XXX Naive implementation that returns all tracks as changed
- if int(version) < frontend.backend.current_playlist.version:
+ if int(version) < context.backend.current_playlist.version:
cpids = [ct[0] for ct in
- frontend.backend.current_playlist.cp_tracks.get()]
+ context.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
- frontend.backend.current_playlist.tracks.get(), cpids=cpids)
+ context.backend.current_playlist.tracks.get(), cpids=cpids)
-@handle_pattern(r'^plchangesposid "(?P\d+)"$')
-def plchangesposid(frontend, version):
+@handle_request(r'^plchangesposid "(?P\d+)"$')
+def plchangesposid(context, version):
"""
*musicpd.org, current playlist section:*
@@ -333,17 +333,17 @@ def plchangesposid(frontend, version):
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
- if int(version) != frontend.backend.current_playlist.version.get():
+ if int(version) != context.backend.current_playlist.version.get():
result = []
for (position, (cpid, _)) in enumerate(
- frontend.backend.current_playlist.cp_tracks.get()):
+ context.backend.current_playlist.cp_tracks.get()):
result.append((u'cpos', position))
result.append((u'Id', cpid))
return result
-@handle_pattern(r'^shuffle$')
-@handle_pattern(r'^shuffle "(?P\d+):(?P\d+)*"$')
-def shuffle(frontend, start=None, end=None):
+@handle_request(r'^shuffle$')
+@handle_request(r'^shuffle "(?P\d+):(?P\d+)*"$')
+def shuffle(context, start=None, end=None):
"""
*musicpd.org, current playlist section:*
@@ -356,10 +356,10 @@ def shuffle(frontend, start=None, end=None):
start = int(start)
if end is not None:
end = int(end)
- frontend.backend.current_playlist.shuffle(start, end)
+ context.backend.current_playlist.shuffle(start, end)
-@handle_pattern(r'^swap "(?P\d+)" "(?P\d+)"$')
-def swap(frontend, songpos1, songpos2):
+@handle_request(r'^swap "(?P\d+)" "(?P\d+)"$')
+def swap(context, songpos1, songpos2):
"""
*musicpd.org, current playlist section:*
@@ -369,18 +369,18 @@ def swap(frontend, songpos1, songpos2):
"""
songpos1 = int(songpos1)
songpos2 = int(songpos2)
- tracks = frontend.backend.current_playlist.tracks.get()
+ tracks = context.backend.current_playlist.tracks.get()
song1 = tracks[songpos1]
song2 = tracks[songpos2]
del tracks[songpos1]
tracks.insert(songpos1, song2)
del tracks[songpos2]
tracks.insert(songpos2, song1)
- frontend.backend.current_playlist.clear()
- frontend.backend.current_playlist.append(tracks)
+ context.backend.current_playlist.clear()
+ context.backend.current_playlist.append(tracks)
-@handle_pattern(r'^swapid "(?P\d+)" "(?P\d+)"$')
-def swapid(frontend, cpid1, cpid2):
+@handle_request(r'^swapid "(?P\d+)" "(?P\d+)"$')
+def swapid(context, cpid1, cpid2):
"""
*musicpd.org, current playlist section:*
@@ -390,9 +390,9 @@ def swapid(frontend, cpid1, cpid2):
"""
cpid1 = int(cpid1)
cpid2 = int(cpid2)
- cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get()
- cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get()
- cp_tracks = frontend.backend.current_playlist.cp_tracks.get()
+ cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
+ cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
+ cp_tracks = context.backend.current_playlist.cp_tracks.get()
position1 = cp_tracks.index(cp_track1)
position2 = cp_tracks.index(cp_track2)
- swap(frontend, position1, position2)
+ swap(context, position1, position2)
diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py
index a39d79eb..0e418551 100644
--- a/mopidy/frontends/mpd/protocol/empty.py
+++ b/mopidy/frontends/mpd/protocol/empty.py
@@ -1,6 +1,6 @@
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
-@handle_pattern(r'^$')
-def empty(frontend):
+@handle_request(r'^$')
+def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
pass
diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py
index a6836533..0343b3ab 100644
--- a/mopidy/frontends/mpd/protocol/music_db.py
+++ b/mopidy/frontends/mpd/protocol/music_db.py
@@ -1,7 +1,7 @@
import re
import shlex
-from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
+from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
def _build_query(mpd_query):
@@ -28,8 +28,8 @@ def _build_query(mpd_query):
query[field] = [what]
return query
-@handle_pattern(r'^count "(?P[^"]+)" "(?P[^"]*)"$')
-def count(frontend, tag, needle):
+@handle_request(r'^count "(?P[^"]+)" "(?P[^"]*)"$')
+def count(context, tag, needle):
"""
*musicpd.org, music database section:*
@@ -40,10 +40,10 @@ def count(frontend, tag, needle):
"""
return [('songs', 0), ('playtime', 0)] # TODO
-@handle_pattern(r'^find '
+@handle_request(r'^find '
r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
-def find(frontend, mpd_query):
+def find(context, mpd_query):
"""
*musicpd.org, music database section:*
@@ -68,12 +68,12 @@ def find(frontend, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
- return frontend.backend.library.find_exact(**query).get().mpd_format()
+ return context.backend.library.find_exact(**query).get().mpd_format()
-@handle_pattern(r'^findadd '
+@handle_request(r'^findadd '
r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
'"[^"]+"\s?)+)$')
-def findadd(frontend, query):
+def findadd(context, query):
"""
*musicpd.org, music database section:*
@@ -84,11 +84,11 @@ def findadd(frontend, query):
``WHAT`` is what to find.
"""
# TODO Add result to current playlist
- #result = frontend.find(query)
+ #result = context.find(query)
-@handle_pattern(r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
+@handle_request(r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
'( (?P.*))?$')
-def list_(frontend, field, mpd_query=None):
+def list_(context, field, mpd_query=None):
"""
*musicpd.org, music database section:*
@@ -175,11 +175,11 @@ def list_(frontend, field, mpd_query=None):
field = field.lower()
query = _list_build_query(field, mpd_query)
if field == u'artist':
- return _list_artist(frontend, query)
+ return _list_artist(context, query)
elif field == u'album':
- return _list_album(frontend, query)
+ return _list_album(context, query)
elif field == u'date':
- return _list_date(frontend, query)
+ return _list_date(context, query)
elif field == u'genre':
pass # TODO We don't have genre in our internal data structures yet
@@ -213,32 +213,32 @@ def _list_build_query(field, mpd_query):
else:
raise MpdArgError(u'not able to parse args', command=u'list')
-def _list_artist(frontend, query):
+def _list_artist(context, query):
artists = set()
- playlist = frontend.backend.library.find_exact(**query).get()
+ playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
return artists
-def _list_album(frontend, query):
+def _list_album(context, query):
albums = set()
- playlist = frontend.backend.library.find_exact(**query).get()
+ playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.album is not None:
albums.add((u'Album', track.album.name))
return albums
-def _list_date(frontend, query):
+def _list_date(context, query):
dates = set()
- playlist = frontend.backend.library.find_exact(**query).get()
+ playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
return dates
-@handle_pattern(r'^listall "(?P[^"]+)"')
-def listall(frontend, uri):
+@handle_request(r'^listall "(?P[^"]+)"')
+def listall(context, uri):
"""
*musicpd.org, music database section:*
@@ -248,8 +248,8 @@ def listall(frontend, uri):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^listallinfo "(?P[^"]+)"')
-def listallinfo(frontend, uri):
+@handle_request(r'^listallinfo "(?P[^"]+)"')
+def listallinfo(context, uri):
"""
*musicpd.org, music database section:*
@@ -260,9 +260,9 @@ def listallinfo(frontend, uri):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^lsinfo$')
-@handle_pattern(r'^lsinfo "(?P[^"]*)"$')
-def lsinfo(frontend, uri=None):
+@handle_request(r'^lsinfo$')
+@handle_request(r'^lsinfo "(?P[^"]*)"$')
+def lsinfo(context, uri=None):
"""
*musicpd.org, music database section:*
@@ -279,11 +279,11 @@ def lsinfo(frontend, uri=None):
""``, and ``lsinfo "/"``.
"""
if uri is None or uri == u'/' or uri == u'':
- return stored_playlists.listplaylists(frontend)
+ return stored_playlists.listplaylists(context)
raise MpdNotImplemented # TODO
-@handle_pattern(r'^rescan( "(?P[^"]+)")*$')
-def rescan(frontend, uri=None):
+@handle_request(r'^rescan( "(?P[^"]+)")*$')
+def rescan(context, uri=None):
"""
*musicpd.org, music database section:*
@@ -291,12 +291,12 @@ def rescan(frontend, uri=None):
Same as ``update``, but also rescans unmodified files.
"""
- return update(frontend, uri, rescan_unmodified_files=True)
+ return update(context, uri, rescan_unmodified_files=True)
-@handle_pattern(r'^search '
+@handle_request(r'^search '
r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
-def search(frontend, mpd_query):
+def search(context, mpd_query):
"""
*musicpd.org, music database section:*
@@ -324,10 +324,10 @@ def search(frontend, mpd_query):
- also uses the search type "date".
"""
query = _build_query(mpd_query)
- return frontend.backend.library.search(**query).get().mpd_format()
+ return context.backend.library.search(**query).get().mpd_format()
-@handle_pattern(r'^update( "(?P[^"]+)")*$')
-def update(frontend, uri=None, rescan_unmodified_files=False):
+@handle_request(r'^update( "(?P[^"]+)")*$')
+def update(context, uri=None, rescan_unmodified_files=False):
"""
*musicpd.org, music database section:*
diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py
index 65282f42..63cfe649 100644
--- a/mopidy/frontends/mpd/protocol/playback.py
+++ b/mopidy/frontends/mpd/protocol/playback.py
@@ -1,11 +1,11 @@
from mopidy.backends.base import PlaybackController
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
-@handle_pattern(r'^consume (?P[01])$')
-@handle_pattern(r'^consume "(?P[01])"$')
-def consume(frontend, state):
+@handle_request(r'^consume (?P[01])$')
+@handle_request(r'^consume "(?P[01])"$')
+def consume(context, state):
"""
*musicpd.org, playback section:*
@@ -16,12 +16,12 @@ def consume(frontend, state):
playlist.
"""
if int(state):
- frontend.backend.playback.consume = True
+ context.backend.playback.consume = True
else:
- frontend.backend.playback.consume = False
+ context.backend.playback.consume = False
-@handle_pattern(r'^crossfade "(?P\d+)"$')
-def crossfade(frontend, seconds):
+@handle_request(r'^crossfade "(?P\d+)"$')
+def crossfade(context, seconds):
"""
*musicpd.org, playback section:*
@@ -32,8 +32,8 @@ def crossfade(frontend, seconds):
seconds = int(seconds)
raise MpdNotImplemented # TODO
-@handle_pattern(r'^next$')
-def next_(frontend):
+@handle_request(r'^next$')
+def next_(context):
"""
*musicpd.org, playback section:*
@@ -87,11 +87,11 @@ def next_(frontend):
order as the first time.
"""
- return frontend.backend.playback.next().get()
+ return context.backend.playback.next().get()
-@handle_pattern(r'^pause$')
-@handle_pattern(r'^pause "(?P[01])"$')
-def pause(frontend, state=None):
+@handle_request(r'^pause$')
+@handle_request(r'^pause "(?P[01])"$')
+def pause(context, state=None):
"""
*musicpd.org, playback section:*
@@ -104,28 +104,28 @@ def pause(frontend, state=None):
- Calls ``pause`` without any arguments to toogle pause.
"""
if state is None:
- if (frontend.backend.playback.state.get() ==
+ if (context.backend.playback.state.get() ==
PlaybackController.PLAYING):
- frontend.backend.playback.pause()
- elif (frontend.backend.playback.state.get() ==
+ context.backend.playback.pause()
+ elif (context.backend.playback.state.get() ==
PlaybackController.PAUSED):
- frontend.backend.playback.resume()
+ context.backend.playback.resume()
elif int(state):
- frontend.backend.playback.pause()
+ context.backend.playback.pause()
else:
- frontend.backend.playback.resume()
+ context.backend.playback.resume()
-@handle_pattern(r'^play$')
-def play(frontend):
+@handle_request(r'^play$')
+def play(context):
"""
The original MPD server resumes from the paused state on ``play``
without arguments.
"""
- return frontend.backend.playback.play().get()
+ return context.backend.playback.play().get()
-@handle_pattern(r'^playid "(?P\d+)"$')
-@handle_pattern(r'^playid "(?P-1)"$')
-def playid(frontend, cpid):
+@handle_request(r'^playid "(?P\d+)"$')
+@handle_request(r'^playid "(?P-1)"$')
+def playid(context, cpid):
"""
*musicpd.org, playback section:*
@@ -144,16 +144,16 @@ def playid(frontend, cpid):
"""
cpid = int(cpid)
if cpid == -1:
- return _play_minus_one(frontend)
+ return _play_minus_one(context)
try:
- cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
- return frontend.backend.playback.play(cp_track).get()
+ cp_track = context.backend.current_playlist.get(cpid=cpid).get()
+ return context.backend.playback.play(cp_track).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playid')
-@handle_pattern(r'^play (?P-?\d+)$')
-@handle_pattern(r'^play "(?P-?\d+)"$')
-def playpos(frontend, songpos):
+@handle_request(r'^play (?P-?\d+)$')
+@handle_request(r'^play "(?P-?\d+)"$')
+def playpos(context, songpos):
"""
*musicpd.org, playback section:*
@@ -176,29 +176,29 @@ def playpos(frontend, songpos):
"""
songpos = int(songpos)
if songpos == -1:
- return _play_minus_one(frontend)
+ return _play_minus_one(context)
try:
- cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos]
- return frontend.backend.playback.play(cp_track).get()
+ cp_track = context.backend.current_playlist.cp_tracks.get()[songpos]
+ return context.backend.playback.play(cp_track).get()
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
-def _play_minus_one(frontend):
- if (frontend.backend.playback.state.get() == PlaybackController.PLAYING):
+def _play_minus_one(context):
+ if (context.backend.playback.state.get() == PlaybackController.PLAYING):
return # Nothing to do
- elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED):
- return frontend.backend.playback.resume().get()
- elif frontend.backend.playback.current_cp_track.get() is not None:
- cp_track = frontend.backend.playback.current_cp_track.get()
- return frontend.backend.playback.play(cp_track).get()
- elif frontend.backend.current_playlist.cp_tracks.get():
- cp_track = frontend.backend.current_playlist.cp_tracks.get()[0]
- return frontend.backend.playback.play(cp_track).get()
+ elif (context.backend.playback.state.get() == PlaybackController.PAUSED):
+ return context.backend.playback.resume().get()
+ elif context.backend.playback.current_cp_track.get() is not None:
+ cp_track = context.backend.playback.current_cp_track.get()
+ return context.backend.playback.play(cp_track).get()
+ elif context.backend.current_playlist.cp_tracks.get():
+ cp_track = context.backend.current_playlist.cp_tracks.get()[0]
+ return context.backend.playback.play(cp_track).get()
else:
return # Fail silently
-@handle_pattern(r'^previous$')
-def previous(frontend):
+@handle_request(r'^previous$')
+def previous(context):
"""
*musicpd.org, playback section:*
@@ -241,11 +241,11 @@ def previous(frontend):
``previous`` should do a seek to time position 0.
"""
- return frontend.backend.playback.previous().get()
+ return context.backend.playback.previous().get()
-@handle_pattern(r'^random (?P[01])$')
-@handle_pattern(r'^random "(?P[01])"$')
-def random(frontend, state):
+@handle_request(r'^random (?P[01])$')
+@handle_request(r'^random "(?P[01])"$')
+def random(context, state):
"""
*musicpd.org, playback section:*
@@ -254,13 +254,13 @@ def random(frontend, state):
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
- frontend.backend.playback.random = True
+ context.backend.playback.random = True
else:
- frontend.backend.playback.random = False
+ context.backend.playback.random = False
-@handle_pattern(r'^repeat (?P[01])$')
-@handle_pattern(r'^repeat "(?P[01])"$')
-def repeat(frontend, state):
+@handle_request(r'^repeat (?P[01])$')
+@handle_request(r'^repeat "(?P[01])"$')
+def repeat(context, state):
"""
*musicpd.org, playback section:*
@@ -269,12 +269,12 @@ def repeat(frontend, state):
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
- frontend.backend.playback.repeat = True
+ context.backend.playback.repeat = True
else:
- frontend.backend.playback.repeat = False
+ context.backend.playback.repeat = False
-@handle_pattern(r'^replay_gain_mode "(?P(off|track|album))"$')
-def replay_gain_mode(frontend, mode):
+@handle_request(r'^replay_gain_mode "(?P(off|track|album))"$')
+def replay_gain_mode(context, mode):
"""
*musicpd.org, playback section:*
@@ -289,8 +289,8 @@ def replay_gain_mode(frontend, mode):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^replay_gain_status$')
-def replay_gain_status(frontend):
+@handle_request(r'^replay_gain_status$')
+def replay_gain_status(context):
"""
*musicpd.org, playback section:*
@@ -301,9 +301,9 @@ def replay_gain_status(frontend):
"""
return u'off' # TODO
-@handle_pattern(r'^seek (?P\d+) (?P\d+)$')
-@handle_pattern(r'^seek "(?P\d+)" "(?P\d+)"$')
-def seek(frontend, songpos, seconds):
+@handle_request(r'^seek (?P\d+) (?P\d+)$')
+@handle_request(r'^seek "(?P\d+)" "(?P\d+)"$')
+def seek(context, songpos, seconds):
"""
*musicpd.org, playback section:*
@@ -316,12 +316,12 @@ def seek(frontend, songpos, seconds):
- issues ``seek 1 120`` without quotes around the arguments.
"""
- if frontend.backend.playback.current_playlist_position != songpos:
- playpos(frontend, songpos)
- frontend.backend.playback.seek(int(seconds) * 1000)
+ if context.backend.playback.current_playlist_position != songpos:
+ playpos(context, songpos)
+ context.backend.playback.seek(int(seconds) * 1000)
-@handle_pattern(r'^seekid "(?P\d+)" "(?P\d+)"$')
-def seekid(frontend, cpid, seconds):
+@handle_request(r'^seekid "(?P\d+)" "(?P\d+)"$')
+def seekid(context, cpid, seconds):
"""
*musicpd.org, playback section:*
@@ -329,13 +329,13 @@ def seekid(frontend, cpid, seconds):
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
- if frontend.backend.playback.current_cpid != cpid:
- playid(frontend, cpid)
- frontend.backend.playback.seek(int(seconds) * 1000)
+ if context.backend.playback.current_cpid != cpid:
+ playid(context, cpid)
+ context.backend.playback.seek(int(seconds) * 1000)
-@handle_pattern(r'^setvol (?P[-+]*\d+)$')
-@handle_pattern(r'^setvol "(?P[-+]*\d+)"$')
-def setvol(frontend, volume):
+@handle_request(r'^setvol (?P[-+]*\d+)$')
+@handle_request(r'^setvol "(?P[-+]*\d+)"$')
+def setvol(context, volume):
"""
*musicpd.org, playback section:*
@@ -352,11 +352,11 @@ def setvol(frontend, volume):
volume = 0
if volume > 100:
volume = 100
- frontend.mixer.volume = volume
+ context.mixer.volume = volume
-@handle_pattern(r'^single (?P[01])$')
-@handle_pattern(r'^single "(?P[01])"$')
-def single(frontend, state):
+@handle_request(r'^single (?P[01])$')
+@handle_request(r'^single "(?P[01])"$')
+def single(context, state):
"""
*musicpd.org, playback section:*
@@ -367,12 +367,12 @@ def single(frontend, state):
song is repeated if the ``repeat`` mode is enabled.
"""
if int(state):
- frontend.backend.playback.single = True
+ context.backend.playback.single = True
else:
- frontend.backend.playback.single = False
+ context.backend.playback.single = False
-@handle_pattern(r'^stop$')
-def stop(frontend):
+@handle_request(r'^stop$')
+def stop(context):
"""
*musicpd.org, playback section:*
@@ -380,4 +380,4 @@ def stop(frontend):
Stops playing.
"""
- frontend.backend.playback.stop()
+ context.backend.playback.stop()
diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py
index ab782440..920f48a5 100644
--- a/mopidy/frontends/mpd/protocol/reflection.py
+++ b/mopidy/frontends/mpd/protocol/reflection.py
@@ -1,8 +1,8 @@
-from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands
+from mopidy.frontends.mpd.protocol import handle_request, mpd_commands
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
-@handle_pattern(r'^commands$')
-def commands(frontend):
+@handle_request(r'^commands$', auth_required=False)
+def commands(context):
"""
*musicpd.org, reflection section:*
@@ -10,25 +10,34 @@ def commands(frontend):
Shows which commands the current user has access to.
"""
- # FIXME When password auth is turned on and the client is not
- # authenticated, 'commands' should list only the commands the client does
- # have access to. To implement this we need access to the session object to
- # check if the client is authenticated or not.
+ if context.dispatcher.authenticated:
+ command_names = [command.name for command in mpd_commands]
+ else:
+ command_names = [command.name for command in mpd_commands
+ if not command.auth_required]
- sorted_commands = sorted(list(mpd_commands))
+ # No permission to use
+ if 'kill' in command_names:
+ command_names.remove('kill')
# Not shown by MPD in its command list
- sorted_commands.remove('command_list_begin')
- sorted_commands.remove('command_list_ok_begin')
- sorted_commands.remove('command_list_end')
- sorted_commands.remove('idle')
- sorted_commands.remove('noidle')
- sorted_commands.remove('sticker')
+ if 'command_list_begin' in command_names:
+ command_names.remove('command_list_begin')
+ if 'command_list_ok_begin' in command_names:
+ command_names.remove('command_list_ok_begin')
+ if 'command_list_end' in command_names:
+ command_names.remove('command_list_end')
+ if 'idle' in command_names:
+ command_names.remove('idle')
+ if 'noidle' in command_names:
+ command_names.remove('noidle')
+ if 'sticker' in command_names:
+ command_names.remove('sticker')
- return [('command', c) for c in sorted_commands]
+ return [('command', command_name) for command_name in sorted(command_names)]
-@handle_pattern(r'^decoders$')
-def decoders(frontend):
+@handle_request(r'^decoders$')
+def decoders(context):
"""
*musicpd.org, reflection section:*
@@ -46,8 +55,8 @@ def decoders(frontend):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^notcommands$')
-def notcommands(frontend):
+@handle_request(r'^notcommands$', auth_required=False)
+def notcommands(context):
"""
*musicpd.org, reflection section:*
@@ -55,14 +64,19 @@ def notcommands(frontend):
Shows which commands the current user does not have access to.
"""
- # FIXME When password auth is turned on and the client is not
- # authenticated, 'notcommands' should list all the commands the client does
- # not have access to. To implement this we need access to the session
- # object to check if the client is authenticated or not.
- pass
+ if context.dispatcher.authenticated:
+ command_names = []
+ else:
+ command_names = [command.name for command in mpd_commands
+ if command.auth_required]
-@handle_pattern(r'^tagtypes$')
-def tagtypes(frontend):
+ # No permission to use
+ command_names.append('kill')
+
+ return [('command', command_name) for command_name in sorted(command_names)]
+
+@handle_request(r'^tagtypes$')
+def tagtypes(context):
"""
*musicpd.org, reflection section:*
@@ -72,8 +86,8 @@ def tagtypes(frontend):
"""
pass # TODO
-@handle_pattern(r'^urlhandlers$')
-def urlhandlers(frontend):
+@handle_request(r'^urlhandlers$')
+def urlhandlers(context):
"""
*musicpd.org, reflection section:*
@@ -81,4 +95,4 @@ def urlhandlers(frontend):
Gets a list of available URL handlers.
"""
- return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()]
+ return [(u'handler', uri) for uri in context.backend.uri_handlers.get()]
diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py
index a78efc0a..abbb8d7f 100644
--- a/mopidy/frontends/mpd/protocol/status.py
+++ b/mopidy/frontends/mpd/protocol/status.py
@@ -1,9 +1,11 @@
+import pykka.future
+
from mopidy.backends.base import PlaybackController
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
-@handle_pattern(r'^clearerror$')
-def clearerror(frontend):
+@handle_request(r'^clearerror$')
+def clearerror(context):
"""
*musicpd.org, status section:*
@@ -14,8 +16,8 @@ def clearerror(frontend):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^currentsong$')
-def currentsong(frontend):
+@handle_request(r'^currentsong$')
+def currentsong(context):
"""
*musicpd.org, status section:*
@@ -24,15 +26,15 @@ def currentsong(frontend):
Displays the song info of the current song (same song that is
identified in status).
"""
- current_cp_track = frontend.backend.playback.current_cp_track.get()
+ current_cp_track = context.backend.playback.current_cp_track.get()
if current_cp_track is not None:
- return current_cp_track[1].mpd_format(
- position=frontend.backend.playback.current_playlist_position.get(),
- cpid=current_cp_track[0])
+ return current_cp_track.track.mpd_format(
+ position=context.backend.playback.current_playlist_position.get(),
+ cpid=current_cp_track.cpid)
-@handle_pattern(r'^idle$')
-@handle_pattern(r'^idle (?P.+)$')
-def idle(frontend, subsystems=None):
+@handle_request(r'^idle$')
+@handle_request(r'^idle (?P.+)$')
+def idle(context, subsystems=None):
"""
*musicpd.org, status section:*
@@ -67,13 +69,13 @@ def idle(frontend, subsystems=None):
"""
pass # TODO
-@handle_pattern(r'^noidle$')
-def noidle(frontend):
+@handle_request(r'^noidle$')
+def noidle(context):
"""See :meth:`_status_idle`."""
pass # TODO
-@handle_pattern(r'^stats$')
-def stats(frontend):
+@handle_request(r'^stats$')
+def stats(context):
"""
*musicpd.org, status section:*
@@ -98,8 +100,8 @@ def stats(frontend):
'playtime': 0, # TODO
}
-@handle_pattern(r'^status$')
-def status(frontend):
+@handle_request(r'^status$')
+def status(context):
"""
*musicpd.org, status section:*
@@ -130,65 +132,80 @@ def status(frontend):
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
"""
+ futures = {
+ 'current_playlist.tracks': context.backend.current_playlist.tracks,
+ 'current_playlist.version': context.backend.current_playlist.version,
+ 'mixer.volume': context.mixer.volume,
+ 'playback.consume': context.backend.playback.consume,
+ 'playback.random': context.backend.playback.random,
+ 'playback.repeat': context.backend.playback.repeat,
+ 'playback.single': context.backend.playback.single,
+ 'playback.state': context.backend.playback.state,
+ 'playback.current_cp_track': context.backend.playback.current_cp_track,
+ 'playback.current_playlist_position':
+ context.backend.playback.current_playlist_position,
+ 'playback.time_position': context.backend.playback.time_position,
+ }
+ pykka.future.get_all(futures.values())
result = [
- ('volume', _status_volume(frontend)),
- ('repeat', _status_repeat(frontend)),
- ('random', _status_random(frontend)),
- ('single', _status_single(frontend)),
- ('consume', _status_consume(frontend)),
- ('playlist', _status_playlist_version(frontend)),
- ('playlistlength', _status_playlist_length(frontend)),
- ('xfade', _status_xfade(frontend)),
- ('state', _status_state(frontend)),
+ ('volume', _status_volume(futures)),
+ ('repeat', _status_repeat(futures)),
+ ('random', _status_random(futures)),
+ ('single', _status_single(futures)),
+ ('consume', _status_consume(futures)),
+ ('playlist', _status_playlist_version(futures)),
+ ('playlistlength', _status_playlist_length(futures)),
+ ('xfade', _status_xfade(futures)),
+ ('state', _status_state(futures)),
]
- if frontend.backend.playback.current_track.get() is not None:
- result.append(('song', _status_songpos(frontend)))
- result.append(('songid', _status_songid(frontend)))
- if frontend.backend.playback.state.get() in (PlaybackController.PLAYING,
+ if futures['playback.current_cp_track'].get() is not None:
+ result.append(('song', _status_songpos(futures)))
+ result.append(('songid', _status_songid(futures)))
+ if futures['playback.state'].get() in (PlaybackController.PLAYING,
PlaybackController.PAUSED):
- result.append(('time', _status_time(frontend)))
- result.append(('elapsed', _status_time_elapsed(frontend)))
- result.append(('bitrate', _status_bitrate(frontend)))
+ result.append(('time', _status_time(futures)))
+ result.append(('elapsed', _status_time_elapsed(futures)))
+ result.append(('bitrate', _status_bitrate(futures)))
return result
-def _status_bitrate(frontend):
- current_track = frontend.backend.playback.current_track.get()
- if current_track is not None:
- return current_track.bitrate
+def _status_bitrate(futures):
+ current_cp_track = futures['playback.current_cp_track'].get()
+ if current_cp_track is not None:
+ return current_cp_track.track.bitrate
-def _status_consume(frontend):
- if frontend.backend.playback.consume.get():
+def _status_consume(futures):
+ if futures['playback.consume'].get():
return 1
else:
return 0
-def _status_playlist_length(frontend):
- return len(frontend.backend.current_playlist.tracks.get())
+def _status_playlist_length(futures):
+ return len(futures['current_playlist.tracks'].get())
-def _status_playlist_version(frontend):
- return frontend.backend.current_playlist.version.get()
+def _status_playlist_version(futures):
+ return futures['current_playlist.version'].get()
-def _status_random(frontend):
- return int(frontend.backend.playback.random.get())
+def _status_random(futures):
+ return int(futures['playback.random'].get())
-def _status_repeat(frontend):
- return int(frontend.backend.playback.repeat.get())
+def _status_repeat(futures):
+ return int(futures['playback.repeat'].get())
-def _status_single(frontend):
- return int(frontend.backend.playback.single.get())
+def _status_single(futures):
+ return int(futures['playback.single'].get())
-def _status_songid(frontend):
- current_cpid = frontend.backend.playback.current_cpid.get()
- if current_cpid is not None:
- return current_cpid
+def _status_songid(futures):
+ current_cp_track = futures['playback.current_cp_track'].get()
+ if current_cp_track is not None:
+ return current_cp_track.cpid
else:
- return _status_songpos(frontend)
+ return _status_songpos(futures)
-def _status_songpos(frontend):
- return frontend.backend.playback.current_playlist_position.get()
+def _status_songpos(futures):
+ return futures['playback.current_playlist_position'].get()
-def _status_state(frontend):
- state = frontend.backend.playback.state.get()
+def _status_state(futures):
+ state = futures['playback.state'].get()
if state == PlaybackController.PLAYING:
return u'play'
elif state == PlaybackController.STOPPED:
@@ -196,28 +213,28 @@ def _status_state(frontend):
elif state == PlaybackController.PAUSED:
return u'pause'
-def _status_time(frontend):
- return u'%s:%s' % (_status_time_elapsed(frontend) // 1000,
- _status_time_total(frontend) // 1000)
+def _status_time(futures):
+ return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
+ _status_time_total(futures) // 1000)
-def _status_time_elapsed(frontend):
- return frontend.backend.playback.time_position.get()
+def _status_time_elapsed(futures):
+ return futures['playback.time_position'].get()
-def _status_time_total(frontend):
- current_track = frontend.backend.playback.current_track.get()
- if current_track is None:
+def _status_time_total(futures):
+ current_cp_track = futures['playback.current_cp_track'].get()
+ if current_cp_track is None:
return 0
- elif current_track.length is None:
+ elif current_cp_track.track.length is None:
return 0
else:
- return current_track.length
+ return current_cp_track.track.length
-def _status_volume(frontend):
- volume = frontend.mixer.volume.get()
+def _status_volume(futures):
+ volume = futures['mixer.volume'].get()
if volume is not None:
return volume
else:
return 0
-def _status_xfade(frontend):
- return 0 # TODO
+def _status_xfade(futures):
+ return 0 # Not supported
diff --git a/mopidy/frontends/mpd/protocol/stickers.py b/mopidy/frontends/mpd/protocol/stickers.py
index 145665eb..c3663ff1 100644
--- a/mopidy/frontends/mpd/protocol/stickers.py
+++ b/mopidy/frontends/mpd/protocol/stickers.py
@@ -1,9 +1,9 @@
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
-@handle_pattern(r'^sticker delete "(?P[^"]+)" '
+@handle_request(r'^sticker delete "(?P[^"]+)" '
r'"(?P[^"]+)"( "(?P[^"]+)")*$')
-def sticker_delete(frontend, field, uri, name=None):
+def sticker_delete(context, field, uri, name=None):
"""
*musicpd.org, sticker section:*
@@ -14,9 +14,9 @@ def sticker_delete(frontend, field, uri, name=None):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^sticker find "(?P[^"]+)" "(?P[^"]+)" '
+@handle_request(r'^sticker find "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)"$')
-def sticker_find(frontend, field, uri, name):
+def sticker_find(context, field, uri, name):
"""
*musicpd.org, sticker section:*
@@ -28,9 +28,9 @@ def sticker_find(frontend, field, uri, name):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^sticker get "(?P[^"]+)" "(?P[^"]+)" '
+@handle_request(r'^sticker get "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)"$')
-def sticker_get(frontend, field, uri, name):
+def sticker_get(context, field, uri, name):
"""
*musicpd.org, sticker section:*
@@ -40,8 +40,8 @@ def sticker_get(frontend, field, uri, name):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^sticker list "(?P[^"]+)" "(?P[^"]+)"$')
-def sticker_list(frontend, field, uri):
+@handle_request(r'^sticker list "(?P[^"]+)" "(?P[^"]+)"$')
+def sticker_list(context, field, uri):
"""
*musicpd.org, sticker section:*
@@ -51,9 +51,9 @@ def sticker_list(frontend, field, uri):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^sticker set "(?P[^"]+)" "(?P[^"]+)" '
+@handle_request(r'^sticker set "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)" "(?P[^"]+)"$')
-def sticker_set(frontend, field, uri, name, value):
+def sticker_set(context, field, uri, name, value):
"""
*musicpd.org, sticker section:*
diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py
index 6eccffac..0a157f66 100644
--- a/mopidy/frontends/mpd/protocol/stored_playlists.py
+++ b/mopidy/frontends/mpd/protocol/stored_playlists.py
@@ -1,10 +1,10 @@
import datetime as dt
-from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
-@handle_pattern(r'^listplaylist "(?P[^"]+)"$')
-def listplaylist(frontend, name):
+@handle_request(r'^listplaylist "(?P[^"]+)"$')
+def listplaylist(context, name):
"""
*musicpd.org, stored playlists section:*
@@ -19,13 +19,13 @@ def listplaylist(frontend, name):
file: relative/path/to/file3.mp3
"""
try:
- playlist = frontend.backend.stored_playlists.get(name=name).get()
+ playlist = context.backend.stored_playlists.get(name=name).get()
return ['file: %s' % t.uri for t in playlist.tracks]
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
-@handle_pattern(r'^listplaylistinfo "(?P[^"]+)"$')
-def listplaylistinfo(frontend, name):
+@handle_request(r'^listplaylistinfo "(?P[^"]+)"$')
+def listplaylistinfo(context, name):
"""
*musicpd.org, stored playlists section:*
@@ -39,14 +39,14 @@ def listplaylistinfo(frontend, name):
Album, Artist, Track
"""
try:
- playlist = frontend.backend.stored_playlists.get(name=name).get()
+ playlist = context.backend.stored_playlists.get(name=name).get()
return playlist.mpd_format()
except LookupError:
raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo')
-@handle_pattern(r'^listplaylists$')
-def listplaylists(frontend):
+@handle_request(r'^listplaylists$')
+def listplaylists(context):
"""
*musicpd.org, stored playlists section:*
@@ -67,7 +67,7 @@ def listplaylists(frontend):
Last-Modified: 2010-02-06T02:11:08Z
"""
result = []
- for playlist in frontend.backend.stored_playlists.playlists.get():
+ for playlist in context.backend.stored_playlists.playlists.get():
result.append((u'playlist', playlist.name))
last_modified = (playlist.last_modified or
dt.datetime.now()).isoformat()
@@ -79,8 +79,8 @@ def listplaylists(frontend):
result.append((u'Last-Modified', last_modified))
return result
-@handle_pattern(r'^load "(?P[^"]+)"$')
-def load(frontend, name):
+@handle_request(r'^load "(?P[^"]+)"$')
+def load(context, name):
"""
*musicpd.org, stored playlists section:*
@@ -93,13 +93,13 @@ def load(frontend, name):
- ``load`` appends the given playlist to the current playlist.
"""
try:
- playlist = frontend.backend.stored_playlists.get(name=name).get()
- frontend.backend.current_playlist.append(playlist.tracks)
+ playlist = context.backend.stored_playlists.get(name=name).get()
+ context.backend.current_playlist.append(playlist.tracks)
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'load')
-@handle_pattern(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$')
-def playlistadd(frontend, name, uri):
+@handle_request(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$')
+def playlistadd(context, name, uri):
"""
*musicpd.org, stored playlists section:*
@@ -111,8 +111,8 @@ def playlistadd(frontend, name, uri):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^playlistclear "(?P[^"]+)"$')
-def playlistclear(frontend, name):
+@handle_request(r'^playlistclear "(?P[^"]+)"$')
+def playlistclear(context, name):
"""
*musicpd.org, stored playlists section:*
@@ -122,8 +122,8 @@ def playlistclear(frontend, name):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^playlistdelete "(?P[^"]+)" "(?P\d+)"$')
-def playlistdelete(frontend, name, songpos):
+@handle_request(r'^playlistdelete "(?P[^"]+)" "(?P\d+)"$')
+def playlistdelete(context, name, songpos):
"""
*musicpd.org, stored playlists section:*
@@ -133,9 +133,9 @@ def playlistdelete(frontend, name, songpos):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^playlistmove "(?P[^"]+)" '
+@handle_request(r'^playlistmove "(?P[^"]+)" '
r'"(?P\d+)" "(?P\d+)"$')
-def playlistmove(frontend, name, from_pos, to_pos):
+def playlistmove(context, name, from_pos, to_pos):
"""
*musicpd.org, stored playlists section:*
@@ -152,8 +152,8 @@ def playlistmove(frontend, name, from_pos, to_pos):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^rename "(?P[^"]+)" "(?P[^"]+)"$')
-def rename(frontend, old_name, new_name):
+@handle_request(r'^rename "(?P[^"]+)" "(?P[^"]+)"$')
+def rename(context, old_name, new_name):
"""
*musicpd.org, stored playlists section:*
@@ -163,8 +163,8 @@ def rename(frontend, old_name, new_name):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^rm "(?P[^"]+)"$')
-def rm(frontend, name):
+@handle_request(r'^rm "(?P[^"]+)"$')
+def rm(context, name):
"""
*musicpd.org, stored playlists section:*
@@ -174,8 +174,8 @@ def rm(frontend, name):
"""
raise MpdNotImplemented # TODO
-@handle_pattern(r'^save "(?P[^"]+)"$')
-def save(frontend, name):
+@handle_request(r'^save "(?P[^"]+)"$')
+def save(context, name):
"""
*musicpd.org, stored playlists section:*
diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py
index 1be46ef4..4e651ddb 100644
--- a/mopidy/frontends/mpd/server.py
+++ b/mopidy/frontends/mpd/server.py
@@ -52,18 +52,19 @@ class MpdServer(asyncore.dispatcher):
self._format_hostname(settings.MPD_SERVER_HOSTNAME),
settings.MPD_SERVER_PORT)
except IOError, e:
- logger.error(u'MPD server startup failed: %s' % str(e).decode('utf-8'))
+ logger.error(u'MPD server startup failed: %s' %
+ str(e).decode('utf-8'))
sys.exit(1)
def handle_accept(self):
- """Handle new client connection."""
+ """Called by asyncore when a new client connects."""
(client_socket, client_socket_address) = self.accept()
logger.info(u'MPD client connection from [%s]:%s',
client_socket_address[0], client_socket_address[1])
- MpdSession(self, client_socket, client_socket_address).start()
+ MpdSession(self, client_socket, client_socket_address)
def handle_close(self):
- """Handle end of client connection."""
+ """Called by asyncore when the socket is closed."""
self.close()
def _format_hostname(self, hostname):
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
index 5a473eca..53f4cab7 100644
--- a/mopidy/frontends/mpd/session.py
+++ b/mopidy/frontends/mpd/session.py
@@ -1,7 +1,6 @@
import asynchat
import logging
-from mopidy import settings
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent
@@ -22,70 +21,38 @@ class MpdSession(asynchat.async_chat):
self.input_buffer = []
self.authenticated = False
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
- self.dispatcher = MpdDispatcher()
-
- def start(self):
- """Start a new client session."""
- self.send_response(u'OK MPD %s' % VERSION)
+ self.dispatcher = MpdDispatcher(session=self)
+ self.send_response([u'OK MPD %s' % VERSION])
def collect_incoming_data(self, data):
- """Collect incoming data into buffer until a terminator is found."""
+ """Called by asynchat when new data arrives."""
self.input_buffer.append(data)
def found_terminator(self):
- """Handle request when a terminator is found."""
+ """Called by asynchat when a terminator is found in incoming data."""
data = ''.join(self.input_buffer).strip()
self.input_buffer = []
try:
- request = data.decode(ENCODING)
- logger.debug(u'Input from [%s]:%s: %s', self.client_address,
- self.client_port, indent(request))
- self.handle_request(request)
+ self.send_response(self.handle_request(data))
except UnicodeDecodeError as e:
logger.warning(u'Received invalid data: %s', e)
def handle_request(self, request):
- """Handle request by sending it to the MPD frontend."""
- if not self.authenticated:
- (self.authenticated, response) = self.check_password(request)
- if response is not None:
- self.send_response(response)
- return
- response = self.dispatcher.handle_request(request)
+ """Handle the request using the MPD command handlers."""
+ request = request.decode(ENCODING)
+ logger.debug(u'Request from [%s]:%s: %s', self.client_address,
+ self.client_port, indent(request))
+ return self.dispatcher.handle_request(request)
+
+ def send_response(self, response):
+ """
+ Format a response from the MPD command handlers and send it to the
+ client.
+ """
if response is not None:
- self.handle_response(response)
-
- def handle_response(self, response):
- """Handle response from the MPD frontend."""
- self.send_response(LINE_TERMINATOR.join(response))
-
- def send_response(self, output):
- """Send a response to the client."""
- logger.debug(u'Output to [%s]:%s: %s', self.client_address,
- self.client_port, indent(output))
- output = u'%s%s' % (output, LINE_TERMINATOR)
- data = output.encode(ENCODING)
- self.push(data)
-
- def check_password(self, request):
- """
- Takes any request and tries to authenticate the client using it.
-
- :rtype: a two-tuple containing (is_authenticated, response_message). If
- the response_message is :class:`None`, normal processing should
- continue, even though the client may not be authenticated.
- """
- if settings.MPD_SERVER_PASSWORD is None:
- return (True, None)
- command = request.split(' ')[0]
- if command == 'password':
- if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD:
- return (True, u'OK')
- else:
- return (False, u'ACK [3@0] {password} incorrect password')
- if command in ('close', 'commands', 'notcommands', 'ping'):
- return (False, None)
- else:
- return (False,
- u'ACK [4@0] {%(c)s} you don\'t have permission for "%(c)s"' %
- {'c': command})
+ response = LINE_TERMINATOR.join(response)
+ logger.debug(u'Response to [%s]:%s: %s', self.client_address,
+ self.client_port, indent(response))
+ response = u'%s%s' % (response, LINE_TERMINATOR)
+ data = response.encode(ENCODING)
+ self.push(data)
diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py
index 3c8941fa..f52292d2 100644
--- a/mopidy/gstreamer.py
+++ b/mopidy/gstreamer.py
@@ -36,71 +36,71 @@ class GStreamer(ThreadingActor):
def __init__(self):
self._pipeline = None
self._source = None
- self._taginject = None
self._tee = None
self._uridecodebin = None
self._volume = None
+ self._outputs = []
+ self._handlers = {}
def on_start(self):
- self._setup_gstreamer()
+ # **Warning:** :class:`GStreamer` requires
+ # :class:`mopidy.utils.process.GObjectEventThread` to be running. This
+ # is not enforced by :class:`GStreamer` itself.
+ self._setup_pipeline()
+ self._setup_outputs()
+ self._setup_message_processor()
- def _setup_gstreamer(self):
- """
- **Warning:** :class:`GStreamer` requires
- :class:`mopidy.utils.process.GObjectEventThread` to be running. This is
- not enforced by :class:`GStreamer` itself.
- """
+ def _setup_pipeline(self):
description = ' ! '.join([
'uridecodebin name=uri',
'audioconvert name=convert',
'volume name=volume',
- 'taginject name=inject',
'tee name=tee'])
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
self._pipeline = gst.parse_launch(description)
- self._taginject = self._pipeline.get_by_name('inject')
self._tee = self._pipeline.get_by_name('tee')
self._volume = self._pipeline.get_by_name('volume')
self._uridecodebin = self._pipeline.get_by_name('uri')
- self._uridecodebin.connect('notify::source', self._process_new_source)
- self._uridecodebin.connect('pad-added', self._process_new_pad,
+ self._uridecodebin.connect('notify::source', self._on_new_source)
+ self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('convert').get_pad('sink'))
+ def _setup_outputs(self):
for output in settings.OUTPUTS:
- output_cls = get_class(output)()
- output_cls.connect_bin(self._pipeline, self._tee)
+ get_class(output)(self).connect()
- # Setup bus and message processor
+ def _setup_message_processor(self):
bus = self._pipeline.get_bus()
bus.add_signal_watch()
- bus.connect('message', self._process_gstreamer_message)
+ bus.connect('message', self._on_message)
- def _process_new_source(self, element, pad):
- self._source = element.get_by_name('source')
+ def _on_new_source(self, element, pad):
+ self._source = element.get_property('source')
try:
self._source.set_property('caps', default_caps)
except TypeError:
pass
- def _process_new_pad(self, source, pad, target_pad):
+ def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked():
pad.link(target_pad)
- def _process_gstreamer_message(self, bus, message):
- """Process messages from GStreamer."""
+ def _on_message(self, bus, message):
+ if message.src in self._handlers:
+ if self._handlers[message.src](message):
+ return # Message was handeled by output
+
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
'Telling backend ...')
self._get_backend().playback.on_end_of_track()
elif message.type == gst.MESSAGE_ERROR:
- self.stop_playback()
error, debug = message.parse_error()
logger.error(u'%s %s', error, debug)
- # FIXME Should we send 'stop_playback' to the backend here? Can we
- # differentiate on how serious the error is?
+ self.stop_playback()
elif message.type == gst.MESSAGE_WARNING:
error, debug = message.parse_warning()
logger.warning(u'%s %s', error, debug)
@@ -112,16 +112,18 @@ class GStreamer(ThreadingActor):
def set_uri(self, uri):
"""
- Change internal uridecodebin's URI
+ Set URI of audio to be played.
+
+ You *MUST* call :meth:`prepare_change` before calling this method.
:param uri: the URI to play
:type uri: string
"""
self._uridecodebin.set_property('uri', uri)
- def deliver_data(self, capabilities, data):
+ def emit_data(self, capabilities, data):
"""
- Deliver audio data to be played
+ Call this to deliver raw audio data to be played.
:param capabilities: a GStreamer capabilities string
:type capabilities: string
@@ -133,9 +135,10 @@ class GStreamer(ThreadingActor):
self._source.set_property('caps', caps)
self._source.emit('push-buffer', buffer_)
- def end_of_data_stream(self):
+ def emit_end_of_stream(self):
"""
- Add end-of-stream token to source.
+ Put an end-of-stream token on the pipeline. This is typically used in
+ combination with :meth:`emit_data`.
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
@@ -172,18 +175,26 @@ class GStreamer(ThreadingActor):
return handeled
def start_playback(self):
- """Notify GStreamer that it should start playback"""
+ """
+ Notify GStreamer that it should start playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
return self._set_state(gst.STATE_PLAYING)
def pause_playback(self):
- """Notify GStreamer that it should pause playback"""
+ """
+ Notify GStreamer that it should pause playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
return self._set_state(gst.STATE_PAUSED)
def prepare_change(self):
"""
Notify GStreamer that we are about to change state of playback.
- This function always needs to be called before changing URIS or doing
+ This function *MUST* be called before changing URIs or doing
changes like updating data that is being pushed. The reason for this
is that GStreamer will reset all its state when it changes to
:attr:`gst.STATE_READY`.
@@ -191,15 +202,22 @@ class GStreamer(ThreadingActor):
return self._set_state(gst.STATE_READY)
def stop_playback(self):
- """Notify GStreamer that is should stop playback"""
+ """
+ Notify GStreamer that is should stop playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
return self._set_state(gst.STATE_NULL)
def _set_state(self, state):
"""
- Set the GStreamer state. Returns :class:`True` if successful.
+ Internal method for setting the raw GStreamer state.
.. digraph:: gst_state_transitions
+ graph [rankdir="LR"];
+ node [fontsize=10];
+
"NULL" -> "READY"
"PAUSED" -> "PLAYING"
"PAUSED" -> "READY"
@@ -210,7 +228,7 @@ class GStreamer(ThreadingActor):
:param state: State to set pipeline to. One of: `gst.STATE_NULL`,
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
:type state: :class:`gst.State`
- :rtype: :class:`True` or :class:`False`
+ :rtype: :class:`True` if successfull, else :class:`False`
"""
result = self._pipeline.set_state(state)
if result == gst.STATE_CHANGE_FAILURE:
@@ -228,7 +246,7 @@ class GStreamer(ThreadingActor):
def get_volume(self):
"""
- Get volume level for software mixer.
+ Get volume level of the GStreamer software mixer.
:rtype: int in range [0..100]
"""
@@ -236,7 +254,7 @@ class GStreamer(ThreadingActor):
def set_volume(self, volume):
"""
- Set volume level for software mixer.
+ Set volume level of the GStreamer software mixer.
:param volume: the volume in the range [0..100]
:type volume: int
@@ -249,16 +267,122 @@ class GStreamer(ThreadingActor):
"""
Set track metadata for currently playing song.
- Only needs to be called by sources such as appsrc which don't already
- inject tags in pipeline.
+ Only needs to be called by sources such as `appsrc` which do not
+ already inject tags in pipeline, e.g. when using :meth:`emit_data` to
+ deliver raw audio data to GStreamer.
- :param track: Track containing metadata for current song.
+ :param track: the current track
:type track: :class:`mopidy.modes.Track`
"""
- # FIXME what if we want to unset taginject tags?
- tags = u'artist="%(artist)s",title="%(title)s"' % {
- 'artist': u', '.join([a.name for a in track.artists]),
- 'title': track.name,
- }
- logger.debug('Setting tags to: %s', tags)
- self._taginject.set_property('tags', tags)
+ taglist = gst.TagList()
+ artists = [a for a in (track.artists or []) if a.name]
+
+ if artists:
+ taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
+ if track.name:
+ taglist[gst.TAG_TITLE] = track.name
+ if track.album and track.album.name:
+ taglist[gst.TAG_ALBUM] = track.album.name
+
+ event = gst.event_new_tag(taglist)
+ self._pipeline.send_event(event)
+
+ def connect_output(self, output):
+ """
+ Connect output to pipeline.
+
+ :param output: output to connect to the pipeline
+ :type output: :class:`gst.Bin`
+ """
+ self._pipeline.add(output)
+ output.sync_state_with_parent() # Required to add to running pipe
+ gst.element_link_many(self._tee, output)
+ self._outputs.append(output)
+ logger.info('Added %s', output.get_name())
+
+ def list_outputs(self):
+ """
+ Get list with the name of all active outputs.
+
+ :rtype: list of strings
+ """
+ return [output.get_name() for output in self._outputs]
+
+ def remove_output(self, output):
+ """
+ Remove output from our pipeline.
+
+ :param output: output to remove from the pipeline
+ :type output: :class:`gst.Bin`
+ """
+ if output not in self._outputs:
+ raise LookupError('Ouput %s not present in pipeline'
+ % output.get_name)
+ teesrc = output.get_pad('sink').get_peer()
+ handler = teesrc.add_event_probe(self._handle_event_probe)
+
+ struct = gst.Structure('mopidy-unlink-tee')
+ struct.set_value('handler', handler)
+
+ event = gst.event_new_custom(gst.EVENT_CUSTOM_DOWNSTREAM, struct)
+ self._tee.send_event(event)
+
+ def _handle_event_probe(self, teesrc, event):
+ if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'):
+ data = self._get_structure_data(event.get_structure())
+
+ output = teesrc.get_peer().get_parent()
+
+ teesrc.unlink(teesrc.get_peer())
+ teesrc.remove_event_probe(data['handler'])
+
+ output.set_state(gst.STATE_NULL)
+ self._pipeline.remove(output)
+
+ logger.warning('Removed %s', output.get_name())
+ return False
+ return True
+
+ def _get_structure_data(self, struct):
+ # Ugly hack to get around missing get_value in pygst bindings :/
+ data = {}
+ def get_data(key, value):
+ data[key] = value
+ struct.foreach(get_data)
+ return data
+
+ def connect_message_handler(self, element, handler):
+ """
+ Attach custom message handler for given element.
+
+ Hook to allow outputs (or other code) to register custom message
+ handlers for all messages coming from the element in question.
+
+ In the case of outputs, :meth:`mopidy.outputs.BaseOutput.on_connect`
+ should be used to attach such handlers and care should be taken to
+ remove them in :meth:`mopidy.outputs.BaseOutput.on_remove` using
+ :meth:`remove_message_handler`.
+
+ The handler callback will only be given the message in question, and
+ is free to ignore the message. However, if the handler wants to prevent
+ the default handling of the message it should return :class:`True`
+ indicating that the message has been handled.
+
+ Note that there can only be one handler per element.
+
+ :param element: element to watch messages from
+ :type element: :class:`gst.Element`
+ :param handler: callable that takes :class:`gst.Message` and returns
+ :class:`True` if the message has been handeled
+ :type handler: callable
+ """
+ self._handlers[element] = handler
+
+ def remove_message_handler(self, element):
+ """
+ Remove custom message handler.
+
+ :param element: element to remove message handling from.
+ :type element: :class:`gst.Element`
+ """
+ self._handlers.pop(element, None)
diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py
index 6329bbbb..ae4bd031 100644
--- a/mopidy/mixers/alsa.py
+++ b/mopidy/mixers/alsa.py
@@ -51,9 +51,9 @@ class AlsaMixer(ThreadingActor, BaseMixer):
return [settings.MIXER_ALSA_CONTROL]
return [u'Master', u'PCM']
- def _get_volume(self):
+ def get_volume(self):
# FIXME does not seem to see external volume changes.
return self._mixer.getvolume()[0]
- def _set_volume(self, volume):
+ def set_volume(self, volume):
self._mixer.setvolume(volume)
diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py
index 74996cb6..ec3d8ae5 100644
--- a/mopidy/mixers/base.py
+++ b/mopidy/mixers/base.py
@@ -17,9 +17,10 @@ class BaseMixer(object):
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
equal to 0. Values above 100 is equal to 100.
"""
- if self._get_volume() is None:
+ volume = self.get_volume()
+ if volume is None:
return None
- return int(self._get_volume() / self.amplification_factor)
+ return int(volume / self.amplification_factor)
@volume.setter
def volume(self, volume):
@@ -28,9 +29,9 @@ class BaseMixer(object):
volume = 0
elif volume > 100:
volume = 100
- self._set_volume(volume)
+ self.set_volume(volume)
- def _get_volume(self):
+ def get_volume(self):
"""
Return volume as integer in range [0, 100]. :class:`None` if unknown.
@@ -38,7 +39,7 @@ class BaseMixer(object):
"""
raise NotImplementedError
- def _set_volume(self, volume):
+ def set_volume(self, volume):
"""
Set volume as integer in range [0, 100].
diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py
index 3922d20a..d0dc5f54 100644
--- a/mopidy/mixers/denon.py
+++ b/mopidy/mixers/denon.py
@@ -35,14 +35,14 @@ class DenonMixer(ThreadingActor, BaseMixer):
from serial import Serial
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
- def _get_volume(self):
+ def get_volume(self):
self._ensure_open_device()
self._device.write('MV?\r')
vol = str(self._device.readline()[2:4])
logger.debug(u'_get_volume() = %s' % vol)
return self._levels.index(vol)
- def _set_volume(self, volume):
+ def set_volume(self, volume):
# Clamp according to Denon-spec
if volume > 99:
volume = 99
diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py
index 186bc7aa..23f96c4c 100644
--- a/mopidy/mixers/dummy.py
+++ b/mopidy/mixers/dummy.py
@@ -8,8 +8,8 @@ class DummyMixer(ThreadingActor, BaseMixer):
def __init__(self):
self._volume = None
- def _get_volume(self):
+ def get_volume(self):
return self._volume
- def _set_volume(self, volume):
+ def set_volume(self, volume):
self._volume = volume
diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py
index 87602772..523c3387 100644
--- a/mopidy/mixers/gstreamer_software.py
+++ b/mopidy/mixers/gstreamer_software.py
@@ -15,8 +15,8 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
- def _get_volume(self):
+ def get_volume(self):
return self.output.get_volume().get()
- def _set_volume(self, volume):
+ def set_volume(self, volume):
self.output.set_volume(volume).get()
diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py
index 62f38bb7..4dbf27be 100644
--- a/mopidy/mixers/nad.py
+++ b/mopidy/mixers/nad.py
@@ -40,10 +40,10 @@ class NadMixer(ThreadingActor, BaseMixer):
self._volume_cache = None
self._nad_talker = NadTalker.start().proxy()
- def _get_volume(self):
+ def get_volume(self):
return self._volume_cache
- def _set_volume(self, volume):
+ def set_volume(self, volume):
self._volume_cache = volume
self._nad_talker.set_volume(volume)
diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py
index 53983095..bd97d790 100644
--- a/mopidy/mixers/osa.py
+++ b/mopidy/mixers/osa.py
@@ -28,7 +28,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
and self._last_update is not None
and (int(time.time() - self._last_update) < self.CACHE_TTL))
- def _get_volume(self):
+ def get_volume(self):
if not self._valid_cache():
try:
self._cache = int(Popen(
@@ -40,7 +40,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
self._last_update = int(time.time())
return self._cache
- def _set_volume(self, volume):
+ def set_volume(self, volume):
Popen(['osascript', '-e', 'set volume output volume %d' % volume])
self._cache = volume
self._last_update = int(time.time())
diff --git a/mopidy/models.py b/mopidy/models.py
index ef60ebbe..ed323b71 100644
--- a/mopidy/models.py
+++ b/mopidy/models.py
@@ -1,4 +1,4 @@
-from mopidy.frontends.mpd import translator
+from collections import namedtuple
class ImmutableObject(object):
"""
@@ -129,6 +129,9 @@ class Album(ImmutableObject):
super(Album, self).__init__(*args, **kwargs)
+CpTrack = namedtuple('CpTrack', ['cpid', 'track'])
+
+
class Track(ImmutableObject):
"""
:param uri: track URI
@@ -183,6 +186,7 @@ class Track(ImmutableObject):
super(Track, self).__init__(*args, **kwargs)
def mpd_format(self, *args, **kwargs):
+ from mopidy.frontends.mpd import translator
return translator.track_to_mpd_format(self, *args, **kwargs)
@@ -222,4 +226,5 @@ class Playlist(ImmutableObject):
return len(self.tracks)
def mpd_format(self, *args, **kwargs):
+ from mopidy.frontends.mpd import translator
return translator.playlist_to_mpd_format(self, *args, **kwargs)
diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py
index d1512617..ba242c4b 100644
--- a/mopidy/outputs/__init__.py
+++ b/mopidy/outputs/__init__.py
@@ -1,153 +1,105 @@
-import logging
-
import pygst
pygst.require('0.10')
import gst
-from mopidy import settings
+import logging
logger = logging.getLogger('mopidy.outputs')
-
class BaseOutput(object):
- """Base class for providing support for multiple pluggable outputs."""
+ """Base class for pluggable audio outputs."""
- def connect_bin(self, pipeline, element):
- """
- Connect output bin to pipeline and given element.
+ MESSAGE_EOS = gst.MESSAGE_EOS
+ MESSAGE_ERROR = gst.MESSAGE_ERROR
+ MESSAGE_WARNING = gst.MESSAGE_WARNING
- In normal cases the element will probably be a `tee`,
- thus allowing us to connect any number of outputs. This
- however is why each bin is forced to have its own `queue`
- after the `tee`.
+ def __init__(self, gstreamer):
+ self.gstreamer = gstreamer
+ self.bin = self._build_bin()
+ self.bin.set_name(self.get_name())
- :param pipeline: gst.Pipeline to add output to.
- :type pipeline: :class:`gst.Pipeline`
- :param element: gst.Element in pipeline to connect output to.
- :type element: :class:`gst.Element`
- """
+ self.modify_bin()
+
+ def _build_bin(self):
description = 'queue ! %s' % self.describe_bin()
- logger.debug('Adding new output to tee: %s', description)
+ logger.debug('Creating new output: %s', description)
+ return gst.parse_bin_from_description(description, True)
- output = gst.parse_bin_from_description(description, True)
- self.modify_bin(output)
+ def connect(self):
+ """Attach output to GStreamer pipeline."""
+ self.gstreamer.connect_output(self.bin)
+ self.on_connect()
- pipeline.add(output)
- output.sync_state_with_parent() # Required to add to running pipe
- gst.element_link_many(element, output)
-
- def modify_bin(self, output):
+ def on_connect(self):
"""
- Modifies bin before it is installed if needed.
+ Called after output has been connected to GStreamer pipeline.
+
+ *MAY be implemented by subclass.*
+ """
+ pass
+
+ def remove(self):
+ """Remove output from GStreamer pipeline."""
+ self.gstreamer.remove_output(self.bin)
+ self.on_remove()
+
+ def on_remove(self):
+ """
+ Called after output has been removed from GStreamer pipeline.
+
+ *MAY be implemented by subclass.*
+ """
+ pass
+
+ def get_name(self):
+ """
+ Get name of the output. Defaults to the output's class name.
+
+ *MAY be implemented by subclass.*
+
+ :rtype: string
+ """
+ return self.__class__.__name__
+
+ def modify_bin(self):
+ """
+ Modifies ``self.bin`` before it is installed if needed.
Overriding this method allows for outputs to modify the constructed bin
before it is installed. This can for instance be a good place to call
`set_properties` on elements that need to be configured.
- :param output: gst.Bin to modify in some way.
- :type output: :class:`gst.Bin`
+ *MAY be implemented by subclass.*
"""
pass
def describe_bin(self):
"""
- Return text string describing bin in gst-launch format.
+ Return string describing the output bin in :command:`gst-launch`
+ format.
- For simple cases this can just be a plain sink such as `autoaudiosink`
- or it can be a chain `element1 ! element2 ! sink`. See `man
- gst-launch0.10` for details on format.
+ For simple cases this can just be a sink such as ``autoaudiosink``,
+ or it can be a chain like ``element1 ! element2 ! sink``. See the
+ manpage of :command:`gst-launch` for details on the format.
*MUST be implemented by subclass.*
+
+ :rtype: string
"""
raise NotImplementedError
def set_properties(self, element, properties):
"""
- Helper to allow for simple setting of properties on elements.
+ Helper method for setting of properties on elements.
- Will call `set_property` on the element for each key that has a value
- that is not None.
+ Will call :meth:`gst.Element.set_property` on ``element`` for each key
+ in ``properties`` that has a value that is not :class:`None`.
- :param element: gst.Element to set properties on.
+ :param element: element to set properties on
:type element: :class:`gst.Element`
- :param properties: Dictionary of properties to set on element.
+ :param properties: properties to set on element
:type properties: dict
"""
for key, value in properties.items():
if value is not None:
element.set_property(key, value)
-
-
-class CustomOutput(BaseOutput):
- """
- Custom output for using alternate setups.
-
- This output is intended to handle two main cases:
-
- 1. Simple things like switching which sink to use. Say :class:`LocalOutput`
- doesn't work for you and you want to switch to ALSA, simple. Set
- :attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good
- to go. Some possible sinks include:
-
- - alsasink
- - osssink
- - pulsesink
- - ...and many more
-
- 2. Advanced setups that require complete control of the output bin. For
- these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
- :command:`gst-launch` compatible string describing the target setup.
-
- """
- def describe_bin(self):
- return settings.CUSTOM_OUTPUT
-
-
-class LocalOutput(BaseOutput):
- """
- Basic output to local audio sink.
-
- This output will normally tell GStreamer to choose whatever it thinks is
- best for your system. In other words this is usually a sane choice.
- """
-
- def describe_bin(self):
- return 'autoaudiosink'
-
-
-class NullOutput(BaseOutput):
- """
- Fall-back null output.
-
- This output will not output anything. It is intended as a fall-back for
- when setup of all other outputs have failed and should not be used by end
- users. Inserting this output in such a case ensures that the pipeline does
- not crash.
- """
-
- def describe_bin(self):
- return 'fakesink'
-
-
-class ShoutcastOutput(BaseOutput):
- """
- Shoutcast streaming output.
-
- This output allows for streaming to an icecast server or anything else that
- supports Shoutcast. The output supports setting for: server address, port,
- mount point, user, password and encoder to use. Please see
- :class:`mopidy.settings` for details about settings.
- """
-
- def describe_bin(self):
- return 'audioconvert ! %s ! shout2send name=shoutcast' \
- % settings.SHOUTCAST_OUTPUT_ENCODER
-
- def modify_bin(self, output):
- self.set_properties(output.get_by_name('shoutcast'), {
- u'ip': settings.SHOUTCAST_OUTPUT_SERVER,
- u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
- u'port': settings.SHOUTCAST_OUTPUT_PORT,
- u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
- u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
- })
diff --git a/mopidy/outputs/custom.py b/mopidy/outputs/custom.py
new file mode 100644
index 00000000..c5ca30bb
--- /dev/null
+++ b/mopidy/outputs/custom.py
@@ -0,0 +1,26 @@
+from mopidy import settings
+from mopidy.outputs import BaseOutput
+
+class CustomOutput(BaseOutput):
+ """
+ Custom output for using alternate setups.
+
+ This output is intended to handle two main cases:
+
+ 1. Simple things like switching which sink to use. Say :class:`LocalOutput`
+ doesn't work for you and you want to switch to ALSA, simple. Set
+ :attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good
+ to go. Some possible sinks include:
+
+ - alsasink
+ - osssink
+ - pulsesink
+ - ...and many more
+
+ 2. Advanced setups that require complete control of the output bin. For
+ these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
+ :command:`gst-launch` compatible string describing the target setup.
+
+ """
+ def describe_bin(self):
+ return settings.CUSTOM_OUTPUT
diff --git a/mopidy/outputs/local.py b/mopidy/outputs/local.py
new file mode 100644
index 00000000..e004a076
--- /dev/null
+++ b/mopidy/outputs/local.py
@@ -0,0 +1,12 @@
+from mopidy.outputs import BaseOutput
+
+class LocalOutput(BaseOutput):
+ """
+ Basic output to local audio sink.
+
+ This output will normally tell GStreamer to choose whatever it thinks is
+ best for your system. In other words this is usually a sane choice.
+ """
+
+ def describe_bin(self):
+ return 'autoaudiosink'
diff --git a/mopidy/outputs/shoutcast.py b/mopidy/outputs/shoutcast.py
new file mode 100644
index 00000000..4298bba5
--- /dev/null
+++ b/mopidy/outputs/shoutcast.py
@@ -0,0 +1,45 @@
+import logging
+
+from mopidy import settings
+from mopidy.outputs import BaseOutput
+
+logger = logging.getLogger('mopidy.outputs.shoutcast')
+
+class ShoutcastOutput(BaseOutput):
+ """
+ Shoutcast streaming output.
+
+ This output allows for streaming to an icecast server or anything else that
+ supports Shoutcast. The output supports setting for: server address, port,
+ mount point, user, password and encoder to use. Please see
+ :class:`mopidy.settings` for details about settings.
+ """
+
+ def describe_bin(self):
+ return 'audioconvert ! %s ! shout2send name=shoutcast' \
+ % settings.SHOUTCAST_OUTPUT_ENCODER
+
+ def modify_bin(self):
+ self.set_properties(self.bin.get_by_name('shoutcast'), {
+ u'ip': settings.SHOUTCAST_OUTPUT_SERVER,
+ u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
+ u'port': settings.SHOUTCAST_OUTPUT_PORT,
+ u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
+ u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
+ })
+
+ def on_connect(self):
+ self.gstreamer.connect_message_handler(
+ self.bin.get_by_name('shoutcast'), self.message_handler)
+
+ def on_remove(self):
+ self.gstreamer.remove_message_handler(
+ self.bin.get_by_name('shoutcast'))
+
+ def message_handler(self, message):
+ if message.type != self.MESSAGE_ERROR:
+ return False
+ error, debug = message.parse_error()
+ logger.warning('%s (%s)', error, debug)
+ self.remove()
+ return True
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 3b5a3bc8..0ac74c82 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -174,10 +174,10 @@ MPD_SERVER_PORT = 6600
#: Default::
#:
#: OUTPUTS = (
-#: u'mopidy.outputs.LocalOutput',
+#: u'mopidy.outputs.local.LocalOutput',
#: )
OUTPUTS = (
- u'mopidy.outputs.LocalOutput',
+ u'mopidy.outputs.local.LocalOutput',
)
#: Servar that runs Shoutcast server to send stream to.
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index dbc6cada..7f6cf664 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -4,6 +4,8 @@ import threading
import gobject
gobject.threads_init()
+from pykka import ActorDeadError
+
from mopidy import SettingsError
logger = logging.getLogger('mopidy.utils.process')
@@ -21,26 +23,19 @@ class BaseThread(threading.Thread):
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'Interrupted by user')
- self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
- self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
- self.exit(2, u'Import error')
+ except ActorDeadError as e:
+ logger.warning(e)
except Exception as e:
logger.exception(e)
- self.exit(3, u'Unknown error')
+ logger.debug(u'%s: Exiting thread', self.name)
def run_inside_try(self):
raise NotImplementedError
- def destroy(self):
- pass
-
- def exit(self, status=0, reason=None):
- self.destroy()
-
class GObjectEventThread(BaseThread):
"""
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index 7f541c21..2bd6e6f3 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import
from copy import copy
import logging
import os
+from pprint import pformat
import sys
from mopidy import SettingsError
@@ -140,19 +141,22 @@ def list_settings_optparse_callback(*args):
option.
"""
from mopidy import settings
+ print format_settings_list(settings)
+ sys.exit(0)
+
+def format_settings_list(settings):
errors = settings.get_errors()
lines = []
for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key)
- value = mask_value_if_secret(key, value)
- lines.append(u'%s:' % key)
- lines.append(u' Value: %s' % repr(value))
+ masked_value = mask_value_if_secret(key, value)
+ lines.append(u'%s: %s' % (key, indent(pformat(masked_value), places=2)))
if value != default_value and default_value is not None:
- lines.append(u' Default: %s' % repr(default_value))
+ lines.append(u' Default: %s' %
+ indent(pformat(default_value), places=4))
if errors.get(key) is not None:
lines.append(u' Error: %s' % errors[key])
- print u'Settings: %s' % indent('\n'.join(lines), places=2)
- sys.exit(0)
+ return '\n'.join(lines)
def mask_value_if_secret(key, value):
if key.endswith('PASSWORD') and value:
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index 427ce76d..b84391af 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -23,14 +23,14 @@ class CurrentPlaylistControllerTest(object):
cp_track = self.controller.add(track)
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(cp_track, self.controller.cp_tracks[-1])
- self.assertEqual(track, cp_track[1])
+ self.assertEqual(track, cp_track.track)
def test_add_at_position(self):
for track in self.tracks[:-1]:
cp_track = self.controller.add(track, 0)
self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(cp_track, self.controller.cp_tracks[0])
- self.assertEqual(track, cp_track[1])
+ self.assertEqual(track, cp_track.track)
@populate_playlist
def test_add_at_position_outside_of_playlist(self):
@@ -40,12 +40,12 @@ class CurrentPlaylistControllerTest(object):
@populate_playlist
def test_get_by_cpid(self):
cp_track = self.controller.cp_tracks[1]
- self.assertEqual(cp_track, self.controller.get(cpid=cp_track[0]))
+ self.assertEqual(cp_track, self.controller.get(cpid=cp_track.cpid))
@populate_playlist
def test_get_by_uri(self):
cp_track = self.controller.cp_tracks[1]
- self.assertEqual(cp_track, self.controller.get(uri=cp_track[1].uri))
+ self.assertEqual(cp_track, self.controller.get(uri=cp_track.track.uri))
@populate_playlist
def test_get_by_uri_raises_error_for_invalid_uri(self):
diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py
index afa99d26..82d9e203 100644
--- a/tests/frontends/mpd/audio_output_test.py
+++ b/tests/frontends/mpd/audio_output_test.py
@@ -1,29 +1,29 @@
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_enableoutput(self):
- result = self.h.handle_request(u'enableoutput "0"')
+ result = self.dispatcher.handle_request(u'enableoutput "0"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_disableoutput(self):
- result = self.h.handle_request(u'disableoutput "0"')
+ result = self.dispatcher.handle_request(u'disableoutput "0"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_outputs(self):
- result = self.h.handle_request(u'outputs')
+ result = self.dispatcher.handle_request(u'outputs')
self.assert_(u'outputid: 0' in result)
self.assert_(u'outputname: None' in result)
self.assert_(u'outputenabled: 1' in result)
diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/authentication_test.py
new file mode 100644
index 00000000..d795d726
--- /dev/null
+++ b/tests/frontends/mpd/authentication_test.py
@@ -0,0 +1,63 @@
+import mock
+import unittest
+
+from mopidy import settings
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
+from mopidy.frontends.mpd.session import MpdSession
+
+class AuthenticationTest(unittest.TestCase):
+ def setUp(self):
+ self.session = mock.Mock(spec=MpdSession)
+ self.dispatcher = MpdDispatcher(session=self.session)
+
+ def tearDown(self):
+ settings.runtime.clear()
+
+ def test_authentication_with_valid_password_is_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'password "topsecret"')
+ self.assertTrue(self.dispatcher.authenticated)
+ self.assert_(u'OK' in response)
+
+ def test_authentication_with_invalid_password_is_not_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'password "secret"')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(u'ACK [3@0] {password} incorrect password' in response)
+
+ def test_authentication_with_anything_when_password_check_turned_off(self):
+ settings.MPD_SERVER_PASSWORD = None
+ response = self.dispatcher.handle_request(u'any request at all')
+ self.assertTrue(self.dispatcher.authenticated)
+ self.assert_('ACK [5@0] {} unknown command "any"' in response)
+
+ def test_anything_when_not_authenticated_should_fail(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'any request at all')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(
+ u'ACK [4@0] {any} you don\'t have permission for "any"' in response)
+
+ def test_close_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'close')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(u'OK' in response)
+
+ def test_commands_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'commands')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(u'OK' in response)
+
+ def test_notcommands_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'notcommands')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(u'OK' in response)
+
+ def test_ping_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ response = self.dispatcher.handle_request(u'ping')
+ self.assertFalse(self.dispatcher.authenticated)
+ self.assert_(u'OK' in response)
diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py
index 7ff96bac..8fd4c828 100644
--- a/tests/frontends/mpd/command_list_test.py
+++ b/tests/frontends/mpd/command_list_test.py
@@ -8,55 +8,56 @@ class CommandListsTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = dispatcher.MpdDispatcher()
def tearDown(self):
self.b.stop().get()
self.mixer.stop().get()
def test_command_list_begin(self):
- result = self.h.handle_request(u'command_list_begin')
- self.assert_(result is None)
+ result = self.dispatcher.handle_request(u'command_list_begin')
+ self.assertEquals(result, [])
def test_command_list_end(self):
- self.h.handle_request(u'command_list_begin')
- result = self.h.handle_request(u'command_list_end')
+ self.dispatcher.handle_request(u'command_list_begin')
+ result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result)
def test_command_list_end_without_start_first_is_an_unknown_command(self):
- result = self.h.handle_request(u'command_list_end')
+ result = self.dispatcher.handle_request(u'command_list_end')
self.assertEquals(result[0],
u'ACK [5@0] {} unknown command "command_list_end"')
def test_command_list_with_ping(self):
- self.h.handle_request(u'command_list_begin')
- self.assertEqual([], self.h.command_list)
- self.assertEqual(False, self.h.command_list_ok)
- self.h.handle_request(u'ping')
- self.assert_(u'ping' in self.h.command_list)
- result = self.h.handle_request(u'command_list_end')
+ self.dispatcher.handle_request(u'command_list_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
+ self.dispatcher.handle_request(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result)
- self.assertEqual(False, self.h.command_list)
+ self.assertEqual(False, self.dispatcher.command_list)
def test_command_list_with_error_returns_ack_with_correct_index(self):
- self.h.handle_request(u'command_list_begin')
- self.h.handle_request(u'play') # Known command
- self.h.handle_request(u'paly') # Unknown command
- result = self.h.handle_request(u'command_list_end')
+ self.dispatcher.handle_request(u'command_list_begin')
+ self.dispatcher.handle_request(u'play') # Known command
+ self.dispatcher.handle_request(u'paly') # Unknown command
+ result = self.dispatcher.handle_request(u'command_list_end')
+ self.assertEqual(len(result), 1, result)
self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self):
- result = self.h.handle_request(u'command_list_ok_begin')
- self.assert_(result is None)
+ result = self.dispatcher.handle_request(u'command_list_ok_begin')
+ self.assertEquals(result, [])
def test_command_list_ok_with_ping(self):
- self.h.handle_request(u'command_list_ok_begin')
- self.assertEqual([], self.h.command_list)
- self.assertEqual(True, self.h.command_list_ok)
- self.h.handle_request(u'ping')
- self.assert_(u'ping' in self.h.command_list)
- result = self.h.handle_request(u'command_list_end')
+ self.dispatcher.handle_request(u'command_list_ok_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(True, self.dispatcher.command_list_ok)
+ self.dispatcher.handle_request(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'list_OK' in result)
self.assert_(u'OK' in result)
- self.assertEqual(False, self.h.command_list)
- self.assertEqual(False, self.h.command_list_ok)
+ self.assertEqual(False, self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py
index cf161a5a..bc995a5e 100644
--- a/tests/frontends/mpd/connection_test.py
+++ b/tests/frontends/mpd/connection_test.py
@@ -1,48 +1,53 @@
+import mock
import unittest
from mopidy import settings
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
+from mopidy.frontends.mpd.session import MpdSession
from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.session = mock.Mock(spec=MpdSession)
+ self.dispatcher = MpdDispatcher(session=self.session)
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
settings.runtime.clear()
- def test_close(self):
- result = self.h.handle_request(u'close')
+ def test_close_closes_the_client_connection(self):
+ result = self.dispatcher.handle_request(u'close')
+ self.assert_(self.session.close.called,
+ u'Should call close() on MpdSession')
self.assert_(u'OK' in result)
def test_empty_request(self):
- result = self.h.handle_request(u'')
+ result = self.dispatcher.handle_request(u'')
self.assert_(u'OK' in result)
def test_kill(self):
- result = self.h.handle_request(u'kill')
- self.assert_(u'OK' in result)
+ result = self.dispatcher.handle_request(u'kill')
+ self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
def test_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.h.handle_request(u'password "topsecret"')
+ result = self.dispatcher.handle_request(u'password "topsecret"')
self.assert_(u'OK' in result)
def test_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.h.handle_request(u'password "secret"')
+ result = self.dispatcher.handle_request(u'password "secret"')
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
- result = self.h.handle_request(u'password "secret"')
+ result = self.dispatcher.handle_request(u'password "secret"')
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_ping(self):
- result = self.h.handle_request(u'ping')
+ result = self.dispatcher.handle_request(u'ping')
self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py
index eb113ed7..c7f47429 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/current_playlist_test.py
@@ -1,160 +1,160 @@
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_add(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider.dummy_library = [
+ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()]
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'add "dummy://foo"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'add "dummy://foo"')
self.assertEqual(len(result), 1)
self.assertEqual(result[0], u'OK')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
- self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
def test_add_with_uri_not_found_in_library_should_ack(self):
- result = self.h.handle_request(u'add "dummy://foo"')
+ result = self.dispatcher.handle_request(u'add "dummy://foo"')
self.assertEqual(result[0],
u'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
- result = self.h.handle_request(u'add ""')
+ result = self.dispatcher.handle_request(u'add ""')
# TODO check that we add all tracks (we currently don't)
self.assert_(u'OK' in result)
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider.dummy_library = [
+ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()]
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'addid "dummy://foo"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
- self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0]
- in result)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'addid "dummy://foo"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
+ self.assert_(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[5][0] in result)
self.assert_(u'OK' in result)
def test_addid_with_empty_uri_acks(self):
- result = self.h.handle_request(u'addid ""')
+ result = self.dispatcher.handle_request(u'addid ""')
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider.dummy_library = [
+ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()]
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'addid "dummy://foo" "3"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
- self.assertEqual(self.b.current_playlist.tracks.get()[3], needle)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0]
- in result)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
+ self.assert_(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[3][0] in result)
self.assert_(u'OK' in result)
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider.dummy_library = [
+ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()]
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'addid "dummy://foo" "6"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self):
- result = self.h.handle_request(u'addid "dummy://foo"')
+ result = self.dispatcher.handle_request(u'addid "dummy://foo"')
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_clear(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'clear')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
- self.assertEqual(self.b.playback.current_track.get(), None)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'clear')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
+ self.assertEqual(self.backend.playback.current_track.get(), None)
self.assert_(u'OK' in result)
def test_delete_songpos(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'delete "%d"' %
- self.b.current_playlist.cp_tracks.get()[2][0])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 4)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'delete "%d"' %
+ self.backend.current_playlist.cp_tracks.get()[2][0])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
self.assert_(u'OK' in result)
def test_delete_songpos_out_of_bounds(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'delete "5"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'delete "5"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'delete "1:"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'delete "1:"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
def test_delete_closed_range(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'delete "1:3"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 3)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'delete "1:3"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
self.assert_(u'OK' in result)
def test_delete_range_out_of_bounds(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
- result = self.h.handle_request(u'delete "5:7"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ result = self.dispatcher.handle_request(u'delete "5:7"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
- self.b.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
- result = self.h.handle_request(u'deleteid "1"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
+ self.backend.current_playlist.append([Track(), Track()])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ result = self.dispatcher.handle_request(u'deleteid "1"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
def test_deleteid_does_not_exist(self):
- self.b.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
- result = self.h.handle_request(u'deleteid "12345"')
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
+ self.backend.current_playlist.append([Track(), Track()])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ result = self.dispatcher.handle_request(u'deleteid "12345"')
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'move "1" "0"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'move "1" "0"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'a')
self.assertEqual(tracks[2].name, 'c')
@@ -164,12 +164,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_move_open_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'move "2:" "0"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'move "2:" "0"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'c')
self.assertEqual(tracks[1].name, 'd')
self.assertEqual(tracks[2].name, 'e')
@@ -179,12 +179,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_move_closed_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'move "1:3" "0"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'move "1:3" "0"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'c')
self.assertEqual(tracks[2].name, 'a')
@@ -194,12 +194,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_moveid(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'moveid "4" "2"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'moveid "4" "2"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'e')
@@ -209,30 +209,30 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlist_returns_same_as_playlistinfo(self):
- playlist_result = self.h.handle_request(u'playlist')
- playlistinfo_result = self.h.handle_request(u'playlistinfo')
+ playlist_result = self.dispatcher.handle_request(u'playlist')
+ playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(playlist_result, playlistinfo_result)
def test_playlistfind(self):
- result = self.h.handle_request(u'playlistfind "tag" "needle"')
+ result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistfind_by_filename_not_in_current_playlist(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'playlistfind "filename" "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
def test_playlistfind_by_filename_without_quotes(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'playlistfind filename "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
def test_playlistfind_by_filename_in_current_playlist(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(uri='file:///exists')])
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result)
self.assert_(u'Id: 0' in result)
@@ -240,15 +240,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistid_without_songid(self):
- self.b.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.h.handle_request(u'playlistid')
+ self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ result = self.dispatcher.handle_request(u'playlistid')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'OK' in result)
def test_playlistid_with_songid(self):
- self.b.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.h.handle_request(u'playlistid "1"')
+ self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ result = self.dispatcher.handle_request(u'playlistid "1"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Id: 0' not in result)
self.assert_(u'Title: b' in result)
@@ -256,16 +256,16 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistid_with_not_existing_songid_fails(self):
- self.b.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.h.handle_request(u'playlistid "25"')
+ self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ result = self.dispatcher.handle_request(u'playlistid "25"')
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'playlistinfo')
+ result = self.dispatcher.handle_request(u'playlistinfo')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
@@ -275,11 +275,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_songpos(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'playlistinfo "4"')
+ result = self.dispatcher.handle_request(u'playlistinfo "4"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' not in result)
@@ -289,16 +289,16 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
- result1 = self.h.handle_request(u'playlistinfo "-1"')
- result2 = self.h.handle_request(u'playlistinfo')
+ result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
+ result2 = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(result1, result2)
def test_playlistinfo_with_open_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'playlistinfo "2:"')
+ result = self.dispatcher.handle_request(u'playlistinfo "2:"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result)
@@ -308,11 +308,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_closed_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'playlistinfo "2:4"')
+ result = self.dispatcher.handle_request(u'playlistinfo "2:4"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result)
@@ -322,52 +322,53 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
- result = self.h.handle_request(u'playlistinfo "10:20"')
+ result = self.dispatcher.handle_request(u'playlistinfo "10:20"')
self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
- result = self.h.handle_request(u'playlistinfo "0:20"')
+ result = self.dispatcher.handle_request(u'playlistinfo "0:20"')
self.assert_(u'OK' in result)
def test_playlistsearch(self):
- result = self.h.handle_request(u'playlistsearch "any" "needle"')
+ result = self.dispatcher.handle_request(
+ u'playlistsearch "any" "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistsearch_without_quotes(self):
- result = self.h.handle_request(u'playlistsearch any "needle"')
+ result = self.dispatcher.handle_request(u'playlistsearch any "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_plchanges(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.h.handle_request(u'plchanges "0"')
+ result = self.dispatcher.handle_request(u'plchanges "0"')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
def test_plchanges_with_minus_one_returns_entire_playlist(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.h.handle_request(u'plchanges "-1"')
+ result = self.dispatcher.handle_request(u'plchanges "-1"')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
def test_plchanges_without_quotes_works(self):
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.h.handle_request(u'plchanges 0')
+ result = self.dispatcher.handle_request(u'plchanges 0')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
def test_plchangesposid(self):
- self.b.current_playlist.append([Track(), Track(), Track()])
- result = self.h.handle_request(u'plchangesposid "0"')
- cp_tracks = self.b.current_playlist.cp_tracks.get()
+ self.backend.current_playlist.append([Track(), Track(), Track()])
+ result = self.dispatcher.handle_request(u'plchangesposid "0"')
+ cp_tracks = self.backend.current_playlist.cp_tracks.get()
self.assert_(u'cpos: 0' in result)
self.assert_(u'Id: %d' % cp_tracks[0][0]
in result)
@@ -380,24 +381,24 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_shuffle_without_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version.get()
- result = self.h.handle_request(u'shuffle')
- self.assert_(version < self.b.current_playlist.version.get())
+ version = self.backend.current_playlist.version.get()
+ result = self.dispatcher.handle_request(u'shuffle')
+ self.assert_(version < self.backend.current_playlist.version.get())
self.assert_(u'OK' in result)
def test_shuffle_with_open_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version.get()
- result = self.h.handle_request(u'shuffle "4:"')
- self.assert_(version < self.b.current_playlist.version.get())
- tracks = self.b.current_playlist.tracks.get()
+ version = self.backend.current_playlist.version.get()
+ result = self.dispatcher.handle_request(u'shuffle "4:"')
+ self.assert_(version < self.backend.current_playlist.version.get())
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'c')
@@ -405,14 +406,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_shuffle_with_closed_range(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version.get()
- result = self.h.handle_request(u'shuffle "1:3"')
- self.assert_(version < self.b.current_playlist.version.get())
- tracks = self.b.current_playlist.tracks.get()
+ version = self.backend.current_playlist.version.get()
+ result = self.dispatcher.handle_request(u'shuffle "1:3"')
+ self.assert_(version < self.backend.current_playlist.version.get())
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
@@ -420,12 +421,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_swap(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'swap "1" "4"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'swap "1" "4"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
self.assertEqual(tracks[2].name, 'c')
@@ -435,12 +436,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_swapid(self):
- self.b.current_playlist.append([
+ self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'swapid "1" "4"')
- tracks = self.b.current_playlist.tracks.get()
+ result = self.dispatcher.handle_request(u'swapid "1" "4"')
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
self.assertEqual(tracks[2].name, 'c')
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 77e0ddf0..7708ce31 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -1,33 +1,33 @@
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
-from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern
+from mopidy.frontends.mpd.protocol import request_handlers, handle_request
from mopidy.mixers.dummy import DummyMixer
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_register_same_pattern_twice_fails(self):
func = lambda: None
try:
- handle_pattern('a pattern')(func)
- handle_pattern('a pattern')(func)
+ handle_request('a pattern')(func)
+ handle_request('a pattern')(func)
self.fail('Registering a pattern twice shoulde raise ValueError')
except ValueError:
pass
def test_finding_handler_for_unknown_command_raises_exception(self):
try:
- self.h.find_handler('an_unknown_command with args')
+ self.dispatcher._find_handler('an_unknown_command with args')
self.fail('Should raise exception')
except MpdAckError as e:
self.assertEqual(e.get_mpd_ack(),
@@ -37,18 +37,18 @@ class MpdDispatcherTest(unittest.TestCase):
expected_handler = lambda x: None
request_handlers['known_command (?P.+)'] = \
expected_handler
- (handler, kwargs) = self.h.find_handler('known_command an_arg')
+ (handler, kwargs) = self.dispatcher._find_handler('known_command an_arg')
self.assertEqual(handler, expected_handler)
self.assert_('arg1' in kwargs)
self.assertEqual(kwargs['arg1'], 'an_arg')
def test_handling_unknown_request_yields_error(self):
- result = self.h.handle_request('an unhandled request')
+ result = self.dispatcher.handle_request('an unhandled request')
self.assertEqual(result[0], u'ACK [5@0] {} unknown command "an"')
def test_handling_known_request(self):
expected = 'magic'
request_handlers['known request'] = lambda x: expected
- result = self.h.handle_request('known request')
+ result = self.dispatcher.handle_request('known request')
self.assert_(u'OK' in result)
self.assert_(expected in result)
diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py
index ef222d46..df2cd65e 100644
--- a/tests/frontends/mpd/exception_test.py
+++ b/tests/frontends/mpd/exception_test.py
@@ -1,7 +1,7 @@
import unittest
-from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand,
- MpdNotImplemented)
+from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
+ MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self):
@@ -25,10 +25,9 @@ class MpdExceptionsTest(unittest.TestCase):
def test_get_mpd_ack_with_values(self):
try:
- raise MpdAckError('A description', error_code=6, index=7,
- command='foo')
+ raise MpdAckError('A description', index=7, command='foo')
except MpdAckError as e:
- self.assertEqual(e.get_mpd_ack(), u'ACK [6@7] {foo} A description')
+ self.assertEqual(e.get_mpd_ack(), u'ACK [0@7] {foo} A description')
def test_mpd_unknown_command(self):
try:
@@ -36,3 +35,17 @@ class MpdExceptionsTest(unittest.TestCase):
except MpdAckError as e:
self.assertEqual(e.get_mpd_ack(),
u'ACK [5@0] {} unknown command "play"')
+
+ def test_mpd_system_error(self):
+ try:
+ raise MpdSystemError('foo')
+ except MpdSystemError as e:
+ self.assertEqual(e.get_mpd_ack(),
+ u'ACK [52@0] {} foo')
+
+ def test_mpd_permission_error(self):
+ try:
+ raise MpdPermissionError(command='foo')
+ except MpdPermissionError as e:
+ self.assertEqual(e.get_mpd_ack(),
+ u'ACK [4@0] {foo} you don\'t have permission for "foo"')
diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py
index fa5634be..3793db9e 100644
--- a/tests/frontends/mpd/music_db_test.py
+++ b/tests/frontends/mpd/music_db_test.py
@@ -1,390 +1,412 @@
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_count(self):
- result = self.h.handle_request(u'count "tag" "needle"')
+ result = self.dispatcher.handle_request(u'count "tag" "needle"')
self.assert_(u'songs: 0' in result)
self.assert_(u'playtime: 0' in result)
self.assert_(u'OK' in result)
def test_findadd(self):
- result = self.h.handle_request(u'findadd "album" "what"')
+ result = self.dispatcher.handle_request(u'findadd "album" "what"')
self.assert_(u'OK' in result)
def test_listall(self):
- result = self.h.handle_request(u'listall "file:///dev/urandom"')
+ result = self.dispatcher.handle_request(
+ u'listall "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_listallinfo(self):
- result = self.h.handle_request(u'listallinfo "file:///dev/urandom"')
+ result = self.dispatcher.handle_request(
+ u'listallinfo "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo')
- listplaylists_result = self.h.handle_request(u'listplaylists')
+ lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
+ listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo ""')
- listplaylists_result = self.h.handle_request(u'listplaylists')
+ lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
+ listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo "/"')
- listplaylists_result = self.h.handle_request(u'listplaylists')
+ lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
+ listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_update_without_uri(self):
- result = self.h.handle_request(u'update')
+ result = self.dispatcher.handle_request(u'update')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_update_with_uri(self):
- result = self.h.handle_request(u'update "file:///dev/urandom"')
+ result = self.dispatcher.handle_request(u'update "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_without_uri(self):
- result = self.h.handle_request(u'rescan')
+ result = self.dispatcher.handle_request(u'rescan')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_with_uri(self):
- result = self.h.handle_request(u'rescan "file:///dev/urandom"')
+ result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
class MusicDatabaseFindTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_find_album(self):
- result = self.h.handle_request(u'find "album" "what"')
+ result = self.dispatcher.handle_request(u'find "album" "what"')
self.assert_(u'OK' in result)
def test_find_album_without_quotes(self):
- result = self.h.handle_request(u'find album "what"')
+ result = self.dispatcher.handle_request(u'find album "what"')
self.assert_(u'OK' in result)
def test_find_artist(self):
- result = self.h.handle_request(u'find "artist" "what"')
+ result = self.dispatcher.handle_request(u'find "artist" "what"')
self.assert_(u'OK' in result)
def test_find_artist_without_quotes(self):
- result = self.h.handle_request(u'find artist "what"')
+ result = self.dispatcher.handle_request(u'find artist "what"')
self.assert_(u'OK' in result)
def test_find_title(self):
- result = self.h.handle_request(u'find "title" "what"')
+ result = self.dispatcher.handle_request(u'find "title" "what"')
self.assert_(u'OK' in result)
def test_find_title_without_quotes(self):
- result = self.h.handle_request(u'find title "what"')
+ result = self.dispatcher.handle_request(u'find title "what"')
self.assert_(u'OK' in result)
def test_find_date(self):
- result = self.h.handle_request(u'find "date" "2002-01-01"')
+ result = self.dispatcher.handle_request(u'find "date" "2002-01-01"')
self.assert_(u'OK' in result)
def test_find_date_without_quotes(self):
- result = self.h.handle_request(u'find date "2002-01-01"')
+ result = self.dispatcher.handle_request(u'find date "2002-01-01"')
self.assert_(u'OK' in result)
def test_find_date_with_capital_d_and_incomplete_date(self):
- result = self.h.handle_request(u'find Date "2005"')
+ result = self.dispatcher.handle_request(u'find Date "2005"')
self.assert_(u'OK' in result)
def test_find_else_should_fail(self):
- result = self.h.handle_request(u'find "somethingelse" "what"')
+ result = self.dispatcher.handle_request(u'find "somethingelse" "what"')
self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
def test_find_album_and_artist(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'find album "album_what" artist "artist_what"')
self.assert_(u'OK' in result)
class MusicDatabaseListTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_list_foo_returns_ack(self):
- result = self.h.handle_request(u'list "foo"')
+ result = self.dispatcher.handle_request(u'list "foo"')
self.assertEqual(result[0],
u'ACK [2@0] {list} incorrect arguments')
### Artist
def test_list_artist_with_quotes(self):
- result = self.h.handle_request(u'list "artist"')
+ result = self.dispatcher.handle_request(u'list "artist"')
self.assert_(u'OK' in result)
def test_list_artist_without_quotes(self):
- result = self.h.handle_request(u'list artist')
+ result = self.dispatcher.handle_request(u'list artist')
self.assert_(u'OK' in result)
def test_list_artist_without_quotes_and_capitalized(self):
- result = self.h.handle_request(u'list Artist')
+ result = self.dispatcher.handle_request(u'list Artist')
self.assert_(u'OK' in result)
def test_list_artist_with_query_of_one_token(self):
- result = self.h.handle_request(u'list "artist" "anartist"')
+ result = self.dispatcher.handle_request(u'list "artist" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
- result = self.h.handle_request(u'list "artist" "foo" "bar"')
+ result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"')
self.assertEqual(result[0],
u'ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self):
- result = self.h.handle_request(u'list "artist" "artist" "anartist"')
+ result = self.dispatcher.handle_request(
+ u'list "artist" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_artist_by_album(self):
- result = self.h.handle_request(u'list "artist" "album" "analbum"')
+ result = self.dispatcher.handle_request(
+ u'list "artist" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_artist_by_full_date(self):
- result = self.h.handle_request(u'list "artist" "date" "2001-01-01"')
+ result = self.dispatcher.handle_request(
+ u'list "artist" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_artist_by_year(self):
- result = self.h.handle_request(u'list "artist" "date" "2001"')
+ result = self.dispatcher.handle_request(
+ u'list "artist" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_artist_by_genre(self):
- result = self.h.handle_request(u'list "artist" "genre" "agenre"')
+ result = self.dispatcher.handle_request(
+ u'list "artist" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_artist_by_artist_and_album(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'list "artist" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Album
def test_list_album_with_quotes(self):
- result = self.h.handle_request(u'list "album"')
+ result = self.dispatcher.handle_request(u'list "album"')
self.assert_(u'OK' in result)
def test_list_album_without_quotes(self):
- result = self.h.handle_request(u'list album')
+ result = self.dispatcher.handle_request(u'list album')
self.assert_(u'OK' in result)
def test_list_album_without_quotes_and_capitalized(self):
- result = self.h.handle_request(u'list Album')
+ result = self.dispatcher.handle_request(u'list Album')
self.assert_(u'OK' in result)
def test_list_album_with_artist_name(self):
- result = self.h.handle_request(u'list "album" "anartist"')
+ result = self.dispatcher.handle_request(u'list "album" "anartist"')
self.assert_(u'OK' in result)
def test_list_album_by_artist(self):
- result = self.h.handle_request(u'list "album" "artist" "anartist"')
+ result = self.dispatcher.handle_request(
+ u'list "album" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_album_by_album(self):
- result = self.h.handle_request(u'list "album" "album" "analbum"')
+ result = self.dispatcher.handle_request(
+ u'list "album" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_album_by_full_date(self):
- result = self.h.handle_request(u'list "album" "date" "2001-01-01"')
+ result = self.dispatcher.handle_request(
+ u'list "album" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_album_by_year(self):
- result = self.h.handle_request(u'list "album" "date" "2001"')
+ result = self.dispatcher.handle_request(
+ u'list "album" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_album_by_genre(self):
- result = self.h.handle_request(u'list "album" "genre" "agenre"')
+ result = self.dispatcher.handle_request(
+ u'list "album" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_album_by_artist_and_album(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'list "album" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Date
def test_list_date_with_quotes(self):
- result = self.h.handle_request(u'list "date"')
+ result = self.dispatcher.handle_request(u'list "date"')
self.assert_(u'OK' in result)
def test_list_date_without_quotes(self):
- result = self.h.handle_request(u'list date')
+ result = self.dispatcher.handle_request(u'list date')
self.assert_(u'OK' in result)
def test_list_date_without_quotes_and_capitalized(self):
- result = self.h.handle_request(u'list Date')
+ result = self.dispatcher.handle_request(u'list Date')
self.assert_(u'OK' in result)
def test_list_date_with_query_of_one_token(self):
- result = self.h.handle_request(u'list "date" "anartist"')
+ result = self.dispatcher.handle_request(u'list "date" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self):
- result = self.h.handle_request(u'list "date" "artist" "anartist"')
+ result = self.dispatcher.handle_request(
+ u'list "date" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_date_by_album(self):
- result = self.h.handle_request(u'list "date" "album" "analbum"')
+ result = self.dispatcher.handle_request(
+ u'list "date" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_date_by_full_date(self):
- result = self.h.handle_request(u'list "date" "date" "2001-01-01"')
+ result = self.dispatcher.handle_request(
+ u'list "date" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_date_by_year(self):
- result = self.h.handle_request(u'list "date" "date" "2001"')
+ result = self.dispatcher.handle_request(u'list "date" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_date_by_genre(self):
- result = self.h.handle_request(u'list "date" "genre" "agenre"')
+ result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_date_by_artist_and_album(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'list "date" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Genre
def test_list_genre_with_quotes(self):
- result = self.h.handle_request(u'list "genre"')
+ result = self.dispatcher.handle_request(u'list "genre"')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes(self):
- result = self.h.handle_request(u'list genre')
+ result = self.dispatcher.handle_request(u'list genre')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes_and_capitalized(self):
- result = self.h.handle_request(u'list Genre')
+ result = self.dispatcher.handle_request(u'list Genre')
self.assert_(u'OK' in result)
def test_list_genre_with_query_of_one_token(self):
- result = self.h.handle_request(u'list "genre" "anartist"')
+ result = self.dispatcher.handle_request(u'list "genre" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self):
- result = self.h.handle_request(u'list "genre" "artist" "anartist"')
+ result = self.dispatcher.handle_request(
+ u'list "genre" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_genre_by_album(self):
- result = self.h.handle_request(u'list "genre" "album" "analbum"')
+ result = self.dispatcher.handle_request(
+ u'list "genre" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_genre_by_full_date(self):
- result = self.h.handle_request(u'list "genre" "date" "2001-01-01"')
+ result = self.dispatcher.handle_request(
+ u'list "genre" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_genre_by_year(self):
- result = self.h.handle_request(u'list "genre" "date" "2001"')
+ result = self.dispatcher.handle_request(
+ u'list "genre" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_genre_by_genre(self):
- result = self.h.handle_request(u'list "genre" "genre" "agenre"')
+ result = self.dispatcher.handle_request(
+ u'list "genre" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_genre_by_artist_and_album(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'list "genre" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
class MusicDatabaseSearchTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_search_album(self):
- result = self.h.handle_request(u'search "album" "analbum"')
+ result = self.dispatcher.handle_request(u'search "album" "analbum"')
self.assert_(u'OK' in result)
def test_search_album_without_quotes(self):
- result = self.h.handle_request(u'search album "analbum"')
+ result = self.dispatcher.handle_request(u'search album "analbum"')
self.assert_(u'OK' in result)
def test_search_artist(self):
- result = self.h.handle_request(u'search "artist" "anartist"')
+ result = self.dispatcher.handle_request(u'search "artist" "anartist"')
self.assert_(u'OK' in result)
def test_search_artist_without_quotes(self):
- result = self.h.handle_request(u'search artist "anartist"')
+ result = self.dispatcher.handle_request(u'search artist "anartist"')
self.assert_(u'OK' in result)
def test_search_filename(self):
- result = self.h.handle_request(u'search "filename" "afilename"')
+ result = self.dispatcher.handle_request(
+ u'search "filename" "afilename"')
self.assert_(u'OK' in result)
def test_search_filename_without_quotes(self):
- result = self.h.handle_request(u'search filename "afilename"')
+ result = self.dispatcher.handle_request(u'search filename "afilename"')
self.assert_(u'OK' in result)
def test_search_title(self):
- result = self.h.handle_request(u'search "title" "atitle"')
+ result = self.dispatcher.handle_request(u'search "title" "atitle"')
self.assert_(u'OK' in result)
def test_search_title_without_quotes(self):
- result = self.h.handle_request(u'search title "atitle"')
+ result = self.dispatcher.handle_request(u'search title "atitle"')
self.assert_(u'OK' in result)
def test_search_any(self):
- result = self.h.handle_request(u'search "any" "anything"')
+ result = self.dispatcher.handle_request(u'search "any" "anything"')
self.assert_(u'OK' in result)
def test_search_any_without_quotes(self):
- result = self.h.handle_request(u'search any "anything"')
+ result = self.dispatcher.handle_request(u'search any "anything"')
self.assert_(u'OK' in result)
def test_search_date(self):
- result = self.h.handle_request(u'search "date" "2002-01-01"')
+ result = self.dispatcher.handle_request(u'search "date" "2002-01-01"')
self.assert_(u'OK' in result)
def test_search_date_without_quotes(self):
- result = self.h.handle_request(u'search date "2002-01-01"')
+ result = self.dispatcher.handle_request(u'search date "2002-01-01"')
self.assert_(u'OK' in result)
def test_search_date_with_capital_d_and_incomplete_date(self):
- result = self.h.handle_request(u'search Date "2005"')
+ result = self.dispatcher.handle_request(u'search Date "2005"')
self.assert_(u'OK' in result)
def test_search_else_should_fail(self):
- result = self.h.handle_request(u'search "sometype" "something"')
+ result = self.dispatcher.handle_request(
+ u'search "sometype" "something"')
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py
index 8601aa9c..e80943d6 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/playback_test.py
@@ -2,7 +2,7 @@ import unittest
from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
@@ -14,393 +14,378 @@ STOPPED = PlaybackController.STOPPED
class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_consume_off(self):
- result = self.h.handle_request(u'consume "0"')
- self.assertFalse(self.b.playback.consume.get())
+ result = self.dispatcher.handle_request(u'consume "0"')
+ self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_off_without_quotes(self):
- result = self.h.handle_request(u'consume 0')
- self.assertFalse(self.b.playback.consume.get())
+ result = self.dispatcher.handle_request(u'consume 0')
+ self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_on(self):
- result = self.h.handle_request(u'consume "1"')
- self.assertTrue(self.b.playback.consume.get())
+ result = self.dispatcher.handle_request(u'consume "1"')
+ self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_on_without_quotes(self):
- result = self.h.handle_request(u'consume 1')
- self.assertTrue(self.b.playback.consume.get())
+ result = self.dispatcher.handle_request(u'consume 1')
+ self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
def test_crossfade(self):
- result = self.h.handle_request(u'crossfade "10"')
+ result = self.dispatcher.handle_request(u'crossfade "10"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_random_off(self):
- result = self.h.handle_request(u'random "0"')
- self.assertFalse(self.b.playback.random.get())
+ result = self.dispatcher.handle_request(u'random "0"')
+ self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result)
def test_random_off_without_quotes(self):
- result = self.h.handle_request(u'random 0')
- self.assertFalse(self.b.playback.random.get())
+ result = self.dispatcher.handle_request(u'random 0')
+ self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result)
def test_random_on(self):
- result = self.h.handle_request(u'random "1"')
- self.assertTrue(self.b.playback.random.get())
+ result = self.dispatcher.handle_request(u'random "1"')
+ self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result)
def test_random_on_without_quotes(self):
- result = self.h.handle_request(u'random 1')
- self.assertTrue(self.b.playback.random.get())
+ result = self.dispatcher.handle_request(u'random 1')
+ self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result)
def test_repeat_off(self):
- result = self.h.handle_request(u'repeat "0"')
- self.assertFalse(self.b.playback.repeat.get())
+ result = self.dispatcher.handle_request(u'repeat "0"')
+ self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_off_without_quotes(self):
- result = self.h.handle_request(u'repeat 0')
- self.assertFalse(self.b.playback.repeat.get())
+ result = self.dispatcher.handle_request(u'repeat 0')
+ self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_on(self):
- result = self.h.handle_request(u'repeat "1"')
- self.assertTrue(self.b.playback.repeat.get())
+ result = self.dispatcher.handle_request(u'repeat "1"')
+ self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_on_without_quotes(self):
- result = self.h.handle_request(u'repeat 1')
- self.assertTrue(self.b.playback.repeat.get())
+ result = self.dispatcher.handle_request(u'repeat 1')
+ self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
def test_setvol_below_min(self):
- result = self.h.handle_request(u'setvol "-10"')
+ result = self.dispatcher.handle_request(u'setvol "-10"')
self.assert_(u'OK' in result)
self.assertEqual(0, self.mixer.volume.get())
def test_setvol_min(self):
- result = self.h.handle_request(u'setvol "0"')
+ result = self.dispatcher.handle_request(u'setvol "0"')
self.assert_(u'OK' in result)
self.assertEqual(0, self.mixer.volume.get())
def test_setvol_middle(self):
- result = self.h.handle_request(u'setvol "50"')
+ result = self.dispatcher.handle_request(u'setvol "50"')
self.assert_(u'OK' in result)
self.assertEqual(50, self.mixer.volume.get())
def test_setvol_max(self):
- result = self.h.handle_request(u'setvol "100"')
+ result = self.dispatcher.handle_request(u'setvol "100"')
self.assert_(u'OK' in result)
self.assertEqual(100, self.mixer.volume.get())
def test_setvol_above_max(self):
- result = self.h.handle_request(u'setvol "110"')
+ result = self.dispatcher.handle_request(u'setvol "110"')
self.assert_(u'OK' in result)
self.assertEqual(100, self.mixer.volume.get())
def test_setvol_plus_is_ignored(self):
- result = self.h.handle_request(u'setvol "+10"')
+ result = self.dispatcher.handle_request(u'setvol "+10"')
self.assert_(u'OK' in result)
self.assertEqual(10, self.mixer.volume.get())
def test_setvol_without_quotes(self):
- result = self.h.handle_request(u'setvol 50')
+ result = self.dispatcher.handle_request(u'setvol 50')
self.assert_(u'OK' in result)
self.assertEqual(50, self.mixer.volume.get())
def test_single_off(self):
- result = self.h.handle_request(u'single "0"')
- self.assertFalse(self.b.playback.single.get())
+ result = self.dispatcher.handle_request(u'single "0"')
+ self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result)
def test_single_off_without_quotes(self):
- result = self.h.handle_request(u'single 0')
- self.assertFalse(self.b.playback.single.get())
+ result = self.dispatcher.handle_request(u'single 0')
+ self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result)
def test_single_on(self):
- result = self.h.handle_request(u'single "1"')
- self.assertTrue(self.b.playback.single.get())
+ result = self.dispatcher.handle_request(u'single "1"')
+ self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result)
def test_single_on_without_quotes(self):
- result = self.h.handle_request(u'single 1')
- self.assertTrue(self.b.playback.single.get())
+ result = self.dispatcher.handle_request(u'single 1')
+ self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result)
def test_replay_gain_mode_off(self):
- result = self.h.handle_request(u'replay_gain_mode "off"')
+ result = self.dispatcher.handle_request(u'replay_gain_mode "off"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_mode_track(self):
- result = self.h.handle_request(u'replay_gain_mode "track"')
+ result = self.dispatcher.handle_request(u'replay_gain_mode "track"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_mode_album(self):
- result = self.h.handle_request(u'replay_gain_mode "album"')
+ result = self.dispatcher.handle_request(u'replay_gain_mode "album"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_status_default(self):
expected = u'off'
- result = self.h.handle_request(u'replay_gain_status')
+ result = self.dispatcher.handle_request(u'replay_gain_status')
self.assert_(u'OK' in result)
self.assert_(expected in result)
def test_replay_gain_status_off(self):
- raise SkipTest
- expected = u'off'
- self.h._replay_gain_mode(expected)
- result = self.h.handle_request(u'replay_gain_status')
- self.assert_(u'OK' in result)
- self.assert_(expected in result)
+ raise SkipTest # TODO
def test_replay_gain_status_track(self):
- raise SkipTest
- expected = u'track'
- self.h._replay_gain_mode(expected)
- result = self.h.handle_request(u'replay_gain_status')
- self.assert_(u'OK' in result)
- self.assert_(expected in result)
+ raise SkipTest # TODO
def test_replay_gain_status_album(self):
- raise SkipTest
- expected = u'album'
- self.h._replay_gain_mode(expected)
- result = self.h.handle_request(u'replay_gain_status')
- self.assert_(u'OK' in result)
- self.assert_(expected in result)
+ raise SkipTest # TODO
class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_next(self):
- result = self.h.handle_request(u'next')
+ result = self.dispatcher.handle_request(u'next')
self.assert_(u'OK' in result)
def test_pause_off(self):
- self.b.current_playlist.append([Track()])
- self.h.handle_request(u'play "0"')
- self.h.handle_request(u'pause "1"')
- result = self.h.handle_request(u'pause "0"')
+ self.backend.current_playlist.append([Track()])
+ self.dispatcher.handle_request(u'play "0"')
+ self.dispatcher.handle_request(u'pause "1"')
+ result = self.dispatcher.handle_request(u'pause "0"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_pause_on(self):
- self.b.current_playlist.append([Track()])
- self.h.handle_request(u'play "0"')
- result = self.h.handle_request(u'pause "1"')
+ self.backend.current_playlist.append([Track()])
+ self.dispatcher.handle_request(u'play "0"')
+ result = self.dispatcher.handle_request(u'pause "1"')
self.assert_(u'OK' in result)
- self.assertEqual(PAUSED, self.b.playback.state.get())
+ self.assertEqual(PAUSED, self.backend.playback.state.get())
def test_pause_toggle(self):
- self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'play "0"')
+ self.backend.current_playlist.append([Track()])
+ result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- result = self.h.handle_request(u'pause')
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result)
- self.assertEqual(PAUSED, self.b.playback.state.get())
- result = self.h.handle_request(u'pause')
+ self.assertEqual(PAUSED, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_play_without_pos(self):
- self.b.current_playlist.append([Track()])
- self.b.playback.state = PAUSED
- result = self.h.handle_request(u'play')
+ self.backend.current_playlist.append([Track()])
+ self.backend.playback.state = PAUSED
+ result = self.dispatcher.handle_request(u'play')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_play_with_pos(self):
- self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'play "0"')
+ self.backend.current_playlist.append([Track()])
+ result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_play_with_pos_without_quotes(self):
- self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'play 0')
+ self.backend.current_playlist.append([Track()])
+ result = self.dispatcher.handle_request(u'play 0')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_play_with_pos_out_of_bounds(self):
- self.b.current_playlist.append([])
- result = self.h.handle_request(u'play "0"')
+ self.backend.current_playlist.append([])
+ result = self.dispatcher.handle_request(u'play "0"')
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
- self.assertEqual(STOPPED, self.b.playback.state.get())
+ self.assertEqual(STOPPED, self.backend.playback.state.get())
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.b.playback.current_track.get(), None)
- self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.h.handle_request(u'play "-1"')
+ self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get().uri, 'a')
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
- self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.b.playback.current_track.get(), None)
- self.b.playback.play()
- self.b.playback.next()
- self.b.playback.stop()
- self.assertNotEqual(self.b.playback.current_track.get(), None)
- result = self.h.handle_request(u'play "-1"')
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.stop()
+ self.assertNotEqual(self.backend.playback.current_track.get(), None)
+ result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get().uri, 'b')
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
- self.b.current_playlist.clear()
- result = self.h.handle_request(u'play "-1"')
+ self.backend.current_playlist.clear()
+ result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(STOPPED, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get(), None)
+ self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get(), None)
def test_play_minus_is_ignored_if_playing(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.b.playback.seek(30000)
- self.assert_(self.b.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.b.playback.state.get())
- result = self.h.handle_request(u'play "-1"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.backend.playback.seek(30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_play_minus_one_resumes_if_paused(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.b.playback.seek(30000)
- self.assert_(self.b.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.b.playback.state.get())
- self.b.playback.pause()
- self.assertEquals(PAUSED, self.b.playback.state.get())
- result = self.h.handle_request(u'play "-1"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.backend.playback.seek(30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.backend.playback.state.get())
+ self.backend.playback.pause()
+ self.assertEquals(PAUSED, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid(self):
- self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'playid "0"')
+ self.backend.current_playlist.append([Track()])
+ result = self.dispatcher.handle_request(u'playid "0"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.b.playback.current_track.get(), None)
- self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.h.handle_request(u'playid "-1"')
+ self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get().uri, 'a')
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
- self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.b.playback.current_track.get(), None)
- self.b.playback.play()
- self.b.playback.next()
- self.b.playback.stop()
- self.assertNotEqual(self.b.playback.current_track.get(), None)
- result = self.h.handle_request(u'playid "-1"')
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.stop()
+ self.assertNotEqual(self.backend.playback.current_track.get(), None)
+ result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get().uri, 'b')
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
- self.b.current_playlist.clear()
- result = self.h.handle_request(u'playid "-1"')
+ self.backend.current_playlist.clear()
+ result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(STOPPED, self.b.playback.state.get())
- self.assertEqual(self.b.playback.current_track.get(), None)
+ self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertEqual(self.backend.playback.current_track.get(), None)
def test_playid_minus_is_ignored_if_playing(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.b.playback.seek(30000)
- self.assert_(self.b.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.b.playback.state.get())
- result = self.h.handle_request(u'playid "-1"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.backend.playback.seek(30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid_minus_one_resumes_if_paused(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.b.playback.seek(30000)
- self.assert_(self.b.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.b.playback.state.get())
- self.b.playback.pause()
- self.assertEquals(PAUSED, self.b.playback.state.get())
- result = self.h.handle_request(u'playid "-1"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.backend.playback.seek(30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.backend.playback.state.get())
+ self.backend.playback.pause()
+ self.assertEquals(PAUSED, self.backend.playback.state.get())
+ result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(PLAYING, self.b.playback.state.get())
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid_which_does_not_exist(self):
- self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'playid "12345"')
+ self.backend.current_playlist.append([Track()])
+ result = self.dispatcher.handle_request(u'playid "12345"')
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
def test_previous(self):
- result = self.h.handle_request(u'previous')
+ result = self.dispatcher.handle_request(u'previous')
self.assert_(u'OK' in result)
def test_seek(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.h.handle_request(u'seek "0"')
- result = self.h.handle_request(u'seek "0" "30"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.dispatcher.handle_request(u'seek "0"')
+ result = self.dispatcher.handle_request(u'seek "0" "30"')
self.assert_(u'OK' in result)
- self.assert_(self.b.playback.time_position >= 30000)
+ self.assert_(self.backend.playback.time_position >= 30000)
def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000)
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
- result = self.h.handle_request(u'seek "1" "30"')
+ result = self.dispatcher.handle_request(u'seek "1" "30"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.current_track.get(), seek_track)
+ self.assertEqual(self.backend.playback.current_track.get(), seek_track)
def test_seek_without_quotes(self):
- self.b.current_playlist.append([Track(length=40000)])
- self.h.handle_request(u'seek 0')
- result = self.h.handle_request(u'seek 0 30')
+ self.backend.current_playlist.append([Track(length=40000)])
+ self.dispatcher.handle_request(u'seek 0')
+ result = self.dispatcher.handle_request(u'seek 0 30')
self.assert_(u'OK' in result)
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_seekid(self):
- self.b.current_playlist.append([Track(length=40000)])
- result = self.h.handle_request(u'seekid "0" "30"')
+ self.backend.current_playlist.append([Track(length=40000)])
+ result = self.dispatcher.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result)
- self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
- self.b.current_playlist.append(
+ self.backend.current_playlist.append(
[Track(length=40000), seek_track])
- result = self.h.handle_request(u'seekid "1" "30"')
+ result = self.dispatcher.handle_request(u'seekid "1" "30"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.current_cpid.get(), 1)
- self.assertEqual(self.b.playback.current_track.get(), seek_track)
+ self.assertEqual(self.backend.playback.current_cpid.get(), 1)
+ self.assertEqual(self.backend.playback.current_track.get(), seek_track)
def test_stop(self):
- result = self.h.handle_request(u'stop')
+ result = self.dispatcher.handle_request(u'stop')
self.assert_(u'OK' in result)
- self.assertEqual(STOPPED, self.b.playback.state.get())
+ self.assertEqual(STOPPED, self.backend.playback.state.get())
diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py
index be95c49b..2abf5acc 100644
--- a/tests/frontends/mpd/reflection_test.py
+++ b/tests/frontends/mpd/reflection_test.py
@@ -1,25 +1,29 @@
import unittest
+from mopidy import settings
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ settings.runtime.clear()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_commands_returns_list_of_all_commands(self):
- result = self.h.handle_request(u'commands')
+ result = self.dispatcher.handle_request(u'commands')
# Check if some random commands are included
self.assert_(u'command: commands' in result)
self.assert_(u'command: play' in result)
self.assert_(u'command: status' in result)
+ # Check if commands you do not have access to are not present
+ self.assert_(u'command: kill' not in result)
# Check if the blacklisted commands are not present
self.assert_(u'command: command_list_begin' not in result)
self.assert_(u'command: command_list_ok_begin' not in result)
@@ -29,20 +33,47 @@ class ReflectionHandlerTest(unittest.TestCase):
self.assert_(u'command: sticker' not in result)
self.assert_(u'OK' in result)
+ def test_commands_show_less_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ result = self.dispatcher.handle_request(u'commands')
+ # Not requiring auth
+ self.assert_(u'command: close' in result, result)
+ self.assert_(u'command: commands' in result, result)
+ self.assert_(u'command: notcommands' in result, result)
+ self.assert_(u'command: password' in result, result)
+ self.assert_(u'command: ping' in result, result)
+ # Requiring auth
+ self.assert_(u'command: play' not in result, result)
+ self.assert_(u'command: status' not in result, result)
+
def test_decoders(self):
- result = self.h.handle_request(u'decoders')
+ result = self.dispatcher.handle_request(u'decoders')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
- def test_notcommands_returns_only_ok(self):
- result = self.h.handle_request(u'notcommands')
- self.assertEqual(1, len(result))
+ def test_notcommands_returns_only_kill_and_ok(self):
+ result = self.dispatcher.handle_request(u'notcommands')
+ self.assertEqual(2, len(result))
+ self.assert_(u'command: kill' in result)
self.assert_(u'OK' in result)
+ def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ result = self.dispatcher.handle_request(u'notcommands')
+ # Not requiring auth
+ self.assert_(u'command: close' not in result, result)
+ self.assert_(u'command: commands' not in result, result)
+ self.assert_(u'command: notcommands' not in result, result)
+ self.assert_(u'command: password' not in result, result)
+ self.assert_(u'command: ping' not in result, result)
+ # Requiring auth
+ self.assert_(u'command: play' in result, result)
+ self.assert_(u'command: status' in result, result)
+
def test_tagtypes(self):
- result = self.h.handle_request(u'tagtypes')
+ result = self.dispatcher.handle_request(u'tagtypes')
self.assert_(u'OK' in result)
def test_urlhandlers(self):
- result = self.h.handle_request(u'urlhandlers')
+ result = self.dispatcher.handle_request(u'urlhandlers')
self.assert_(u'OK' in result)
self.assert_(u'handler: dummy:' in result)
diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py
index 32e90450..ee363aea 100644
--- a/tests/frontends/mpd/server_test.py
+++ b/tests/frontends/mpd/server_test.py
@@ -44,52 +44,3 @@ class MpdSessionTest(unittest.TestCase):
self.session.input_buffer = ['\xff']
self.session.found_terminator()
self.assertEqual(len(self.session.input_buffer), 0)
-
- def test_authentication_with_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'password "topsecret"')
- self.assertTrue(authed)
- self.assertEqual(u'OK', response)
-
- def test_authentication_with_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'password "secret"')
- self.assertFalse(authed)
- self.assertEqual(u'ACK [3@0] {password} incorrect password', response)
-
- def test_authentication_with_anything_when_password_check_turned_off(self):
- settings.MPD_SERVER_PASSWORD = None
- authed, response = self.session.check_password(u'any request at all')
- self.assertTrue(authed)
- self.assertEqual(None, response)
-
- def test_anything_when_not_authenticated_should_fail(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'any request at all')
- self.assertFalse(authed)
- self.assertEqual(
- u'ACK [4@0] {any} you don\'t have permission for "any"', response)
-
- def test_close_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'close')
- self.assertFalse(authed)
- self.assertEqual(None, response)
-
- def test_commands_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'commands')
- self.assertFalse(authed)
- self.assertEqual(None, response)
-
- def test_notcommands_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'notcommands')
- self.assertFalse(authed)
- self.assertEqual(None, response)
-
- def test_ping_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- authed, response = self.session.check_password(u'ping')
- self.assertFalse(authed)
- self.assertEqual(None, response)
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index 791d734f..a7ed921f 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -2,7 +2,8 @@ import unittest
from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
+from mopidy.frontends.mpd.protocol import status
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
@@ -12,23 +13,24 @@ STOPPED = PlaybackController.STOPPED
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
+ self.context = self.dispatcher.context
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_clearerror(self):
- result = self.h.handle_request(u'clearerror')
+ result = self.dispatcher.handle_request(u'clearerror')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_currentsong(self):
track = Track()
- self.b.current_playlist.append([track])
- self.b.playback.play()
- result = self.h.handle_request(u'currentsong')
+ self.backend.current_playlist.append([track])
+ self.backend.playback.play()
+ result = self.dispatcher.handle_request(u'currentsong')
self.assert_(u'file: ' in result)
self.assert_(u'Time: 0' in result)
self.assert_(u'Artist: ' in result)
@@ -41,27 +43,27 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_currentsong_without_song(self):
- result = self.h.handle_request(u'currentsong')
+ result = self.dispatcher.handle_request(u'currentsong')
self.assert_(u'OK' in result)
def test_idle_without_subsystems(self):
- result = self.h.handle_request(u'idle')
+ result = self.dispatcher.handle_request(u'idle')
self.assert_(u'OK' in result)
def test_idle_with_subsystems(self):
- result = self.h.handle_request(u'idle database playlist')
+ result = self.dispatcher.handle_request(u'idle database playlist')
self.assert_(u'OK' in result)
def test_noidle(self):
- result = self.h.handle_request(u'noidle')
+ result = self.dispatcher.handle_request(u'noidle')
self.assert_(u'OK' in result)
def test_stats_command(self):
- result = self.h.handle_request(u'stats')
+ result = self.dispatcher.handle_request(u'stats')
self.assert_(u'OK' in result)
def test_stats_method(self):
- result = dispatcher.status.stats(self.h)
+ result = status.stats(self.context)
self.assert_('artists' in result)
self.assert_(int(result['artists']) >= 0)
self.assert_('albums' in result)
@@ -78,110 +80,110 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(int(result['playtime']) >= 0)
def test_status_command(self):
- result = self.h.handle_request(u'status')
+ result = self.dispatcher.handle_request(u'status')
self.assert_(u'OK' in result)
def test_status_method_contains_volume_which_defaults_to_0(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 0)
def test_status_method_contains_volume(self):
self.mixer.volume = 17
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 17)
def test_status_method_contains_repeat_is_0(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self):
- self.b.playback.repeat = 1
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.repeat = 1
+ result = dict(status.status(self.context))
self.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 1)
def test_status_method_contains_random_is_0(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('random' in result)
self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self):
- self.b.playback.random = 1
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.random = 1
+ result = dict(status.status(self.context))
self.assert_('random' in result)
self.assertEqual(int(result['random']), 1)
def test_status_method_contains_single(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('single' in result)
self.assert_(int(result['single']) in (0, 1))
def test_status_method_contains_consume_is_0(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('consume' in result)
self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self):
- self.b.playback.consume = 1
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.consume = 1
+ result = dict(status.status(self.context))
self.assert_('consume' in result)
self.assertEqual(int(result['consume']), 1)
def test_status_method_contains_playlist(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('playlist' in result)
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
def test_status_method_contains_playlistlength(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('playlistlength' in result)
self.assert_(int(result['playlistlength']) >= 0)
def test_status_method_contains_xfade(self):
- result = dict(dispatcher.status.status(self.h))
+ result = dict(status.status(self.context))
self.assert_('xfade' in result)
self.assert_(int(result['xfade']) >= 0)
def test_status_method_contains_state_is_play(self):
- self.b.playback.state = PLAYING
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.state = PLAYING
+ result = dict(status.status(self.context))
self.assert_('state' in result)
self.assertEqual(result['state'], 'play')
def test_status_method_contains_state_is_stop(self):
- self.b.playback.state = STOPPED
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.state = STOPPED
+ result = dict(status.status(self.context))
self.assert_('state' in result)
self.assertEqual(result['state'], 'stop')
def test_status_method_contains_state_is_pause(self):
- self.b.playback.state = PLAYING
- self.b.playback.state = PAUSED
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.state = PLAYING
+ self.backend.playback.state = PAUSED
+ result = dict(status.status(self.context))
self.assert_('state' in result)
self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self):
- self.b.current_playlist.append([Track()])
- self.b.playback.play()
- result = dict(dispatcher.status.status(self.h))
+ self.backend.current_playlist.append([Track()])
+ self.backend.playback.play()
+ result = dict(status.status(self.context))
self.assert_('song' in result)
self.assert_(int(result['song']) >= 0)
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
- self.b.current_playlist.append([Track()])
- self.b.playback.play()
- result = dict(dispatcher.status.status(self.h))
+ self.backend.current_playlist.append([Track()])
+ self.backend.playback.play()
+ result = dict(status.status(self.context))
self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self):
- self.b.current_playlist.append([Track(length=None)])
- self.b.playback.play()
- result = dict(dispatcher.status.status(self.h))
+ self.backend.current_playlist.append([Track(length=None)])
+ self.backend.playback.play()
+ result = dict(status.status(self.context))
self.assert_('time' in result)
(position, total) = result['time'].split(':')
position = int(position)
@@ -189,9 +191,9 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total)
def test_status_method_when_playing_contains_time_with_length(self):
- self.b.current_playlist.append([Track(length=10000)])
- self.b.playback.play()
- result = dict(dispatcher.status.status(self.h))
+ self.backend.current_playlist.append([Track(length=10000)])
+ self.backend.playback.play()
+ result = dict(status.status(self.context))
self.assert_('time' in result)
(position, total) = result['time'].split(':')
position = int(position)
@@ -199,15 +201,15 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total)
def test_status_method_when_playing_contains_elapsed(self):
- self.b.playback.state = PAUSED
- self.b.playback.play_time_accumulated = 59123
- result = dict(dispatcher.status.status(self.h))
+ self.backend.playback.state = PAUSED
+ self.backend.playback.play_time_accumulated = 59123
+ result = dict(status.status(self.context))
self.assert_('elapsed' in result)
self.assertEqual(int(result['elapsed']), 59123)
def test_status_method_when_playing_contains_bitrate(self):
- self.b.current_playlist.append([Track(bitrate=320)])
- self.b.playback.play()
- result = dict(dispatcher.status.status(self.h))
+ self.backend.current_playlist.append([Track(bitrate=320)])
+ self.backend.playback.play()
+ result = dict(status.status(self.context))
self.assert_('bitrate' in result)
self.assertEqual(int(result['bitrate']), 320)
diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py
index 83d43792..86ac8aec 100644
--- a/tests/frontends/mpd/stickers_test.py
+++ b/tests/frontends/mpd/stickers_test.py
@@ -1,45 +1,45 @@
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_sticker_get(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker get "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_set(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_with_name(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker delete "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_without_name(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker delete "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_list(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker list "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_find(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'sticker find "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py
index e981c9ed..04bab6f1 100644
--- a/tests/frontends/mpd/stored_playlists_test.py
+++ b/tests/frontends/mpd/stored_playlists_test.py
@@ -2,64 +2,64 @@ import datetime as dt
import unittest
from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist
class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend.start().proxy()
+ self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
- self.h = dispatcher.MpdDispatcher()
+ self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.b.stop().get()
+ self.backend.stop().get()
self.mixer.stop().get()
def test_listplaylist(self):
- self.b.stored_playlists.playlists = [
+ self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.h.handle_request(u'listplaylist "name"')
+ result = self.dispatcher.handle_request(u'listplaylist "name"')
self.assert_(u'file: file:///dev/urandom' in result)
self.assert_(u'OK' in result)
def test_listplaylist_fails_if_no_playlist_is_found(self):
- result = self.h.handle_request(u'listplaylist "name"')
+ result = self.dispatcher.handle_request(u'listplaylist "name"')
self.assertEqual(result[0],
u'ACK [50@0] {listplaylist} No such playlist')
def test_listplaylistinfo(self):
- self.b.stored_playlists.playlists = [
+ self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.h.handle_request(u'listplaylistinfo "name"')
+ result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
self.assert_(u'file: file:///dev/urandom' in result)
self.assert_(u'Track: 0' in result)
self.assert_(u'Pos: 0' not in result)
self.assert_(u'OK' in result)
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
- result = self.h.handle_request(u'listplaylistinfo "name"')
+ result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
self.assertEqual(result[0],
u'ACK [50@0] {listplaylistinfo} No such playlist')
def test_listplaylists(self):
last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345)
- self.b.stored_playlists.playlists = [Playlist(name='a',
+ self.backend.stored_playlists.playlists = [Playlist(name='a',
last_modified=last_modified)]
- result = self.h.handle_request(u'listplaylists')
+ result = self.dispatcher.handle_request(u'listplaylists')
self.assert_(u'playlist: a' in result)
# Date without microseconds and with time zone information
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
self.assert_(u'OK' in result)
def test_load_known_playlist_appends_to_current_playlist(self):
- self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
- self.b.stored_playlists.playlists = [Playlist(name='A-list',
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ self.backend.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
- result = self.h.handle_request(u'load "A-list"')
+ result = self.dispatcher.handle_request(u'load "A-list"')
self.assert_(u'OK' in result)
- tracks = self.b.current_playlist.tracks.get()
+ tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(len(tracks), 5)
self.assertEqual(tracks[0].uri, 'a')
self.assertEqual(tracks[1].uri, 'b')
@@ -68,35 +68,35 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
self.assertEqual(tracks[4].uri, 'e')
def test_load_unknown_playlist_acks(self):
- result = self.h.handle_request(u'load "unknown playlist"')
+ result = self.dispatcher.handle_request(u'load "unknown playlist"')
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
- self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
def test_playlistadd(self):
- result = self.h.handle_request(
+ result = self.dispatcher.handle_request(
u'playlistadd "name" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistclear(self):
- result = self.h.handle_request(u'playlistclear "name"')
+ result = self.dispatcher.handle_request(u'playlistclear "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistdelete(self):
- result = self.h.handle_request(u'playlistdelete "name" "5"')
+ result = self.dispatcher.handle_request(u'playlistdelete "name" "5"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistmove(self):
- result = self.h.handle_request(u'playlistmove "name" "5" "10"')
+ result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rename(self):
- result = self.h.handle_request(u'rename "old_name" "new_name"')
+ result = self.dispatcher.handle_request(u'rename "old_name" "new_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rm(self):
- result = self.h.handle_request(u'rm "name"')
+ result = self.dispatcher.handle_request(u'rm "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_save(self):
- result = self.h.handle_request(u'save "name"')
+ result = self.dispatcher.handle_request(u'save "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/models_test.py b/tests/models_test.py
index afbf9d50..637a8209 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -1,7 +1,7 @@
import datetime as dt
import unittest
-from mopidy.models import Artist, Album, Track, Playlist
+from mopidy.models import Artist, Album, CpTrack, Track, Playlist
from tests import SkipTest
@@ -274,6 +274,21 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(hash(album1), hash(album2))
+class CpTrackTest(unittest.TestCase):
+ def setUp(self):
+ self.cpid = 123
+ self.track = Track()
+ self.cp_track = CpTrack(self.cpid, self.track)
+
+ def test_cp_track_can_be_accessed_as_a_tuple(self):
+ self.assertEqual(self.cpid, self.cp_track[0])
+ self.assertEqual(self.track, self.cp_track[1])
+
+ def test_cp_track_can_be_accessed_by_attribute_names(self):
+ self.assertEqual(self.cpid, self.cp_track.cpid)
+ self.assertEqual(self.track, self.cp_track.track)
+
+
class TrackTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index 11914f61..1ffff9a6 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -2,8 +2,8 @@ import os
import unittest
from mopidy import settings as default_settings_module, SettingsError
-from mopidy.utils.settings import validate_settings, SettingsProxy
-from mopidy.utils.settings import mask_value_if_secret
+from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
+ SettingsProxy, validate_settings)
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
@@ -140,3 +140,48 @@ class SettingsProxyTest(unittest.TestCase):
self.settings.TEST = './test'
actual = self.settings.TEST
self.assertEqual(actual, './test')
+
+
+class FormatSettingListTest(unittest.TestCase):
+ def setUp(self):
+ self.settings = SettingsProxy(default_settings_module)
+
+ def test_contains_the_setting_name(self):
+ self.settings.TEST = u'test'
+ result = format_settings_list(self.settings)
+ self.assert_('TEST:' in result, result)
+
+ def test_repr_of_a_string_value(self):
+ self.settings.TEST = u'test'
+ result = format_settings_list(self.settings)
+ self.assert_("TEST: u'test'" in result, result)
+
+ def test_repr_of_an_int_value(self):
+ self.settings.TEST = 123
+ result = format_settings_list(self.settings)
+ self.assert_("TEST: 123" in result, result)
+
+ def test_repr_of_a_tuple_value(self):
+ self.settings.TEST = (123, u'abc')
+ result = format_settings_list(self.settings)
+ self.assert_("TEST: (123, u'abc')" in result, result)
+
+ def test_passwords_are_masked(self):
+ self.settings.TEST_PASSWORD = u'secret'
+ result = format_settings_list(self.settings)
+ self.assert_("TEST_PASSWORD: u'secret'" not in result, result)
+ self.assert_("TEST_PASSWORD: u'********'" in result, result)
+
+ def test_short_values_are_not_pretty_printed(self):
+ self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
+ result = format_settings_list(self.settings)
+ self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result,
+ result)
+
+ def test_long_values_are_pretty_printed(self):
+ self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',
+ u'mopidy.frontends.lastfm.LastfmFrontend')
+ result = format_settings_list(self.settings)
+ self.assert_("""FRONTEND:
+ (u'mopidy.frontends.mpd.MpdFrontend',
+ u'mopidy.frontends.lastfm.LastfmFrontend')""" in result, result)