Merge branch 'develop' into feature/mpris-frontend

Conflicts:
	docs/changes.rst
This commit is contained in:
Stein Magnus Jodal 2011-06-07 02:30:01 +02:00
commit 61375f0edc
62 changed files with 1892 additions and 1441 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
*.pyc *.pyc
*.swp *.swp
.coverage .coverage
.idea
.noseids .noseids
.tox .tox
MANIFEST MANIFEST

View File

@ -13,7 +13,6 @@ Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way.
Output implementations Output implementations
====================== ======================
* :class:`mopidy.outputs.CustomOutput` * :class:`mopidy.outputs.custom.CustomOutput`
* :class:`mopidy.outputs.LocalOutput` * :class:`mopidy.outputs.local.LocalOutput`
* :class:`mopidy.outputs.NullOutput` * :class:`mopidy.outputs.shoutcast.ShoutcastOutput`
* :class:`mopidy.outputs.ShoutcastOutput`

View File

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

View File

@ -5,8 +5,8 @@ Changes
This change log is used to track all major changes to Mopidy. 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. No description yet.
@ -24,6 +24,26 @@ No description yet.
- Support passing options to GStreamer. See :option:`--help-gst` for a list of - Support passing options to GStreamer. See :option:`--help-gst` for a list of
available options. (Fixes: :issue:`95`) 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: - Backends:
- Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next` - Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
@ -37,8 +57,8 @@ No description yet.
track without changing the playback state. 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 This is a bug fix release fixing audio problems on older GStreamer and some
minor bugs. minor bugs.
@ -67,8 +87,8 @@ minor bugs.
configured. (Fixes: :issue:`84`) 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 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 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. 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. 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. 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 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 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`. :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. This is a maintenance release without any new features.
@ -348,8 +368,8 @@ This is a maintenance release without any new features.
failure. failure.
0.2.0 (2010-10-24) v0.2.0 (2010-10-24)
================== ===================
In Mopidy 0.2.0 we've added a `Last.fm <http://www.last.fm/>`_ scrobbling In Mopidy 0.2.0 we've added a `Last.fm <http://www.last.fm/>`_ scrobbling
support, which means that Mopidy now can submit meta data about the tracks you 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. 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 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. 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. :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 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 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``. ``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 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. 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. 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 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 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. - 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. "*Release early. Release often. Listen to your customers.*" wrote Eric S.
Raymond in *The Cathedral and the Bazaar*. Raymond in *The Cathedral and the Bazaar*.

View File

@ -25,7 +25,7 @@ import mopidy
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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.graphviz', 'sphinx.ext.inheritance_diagram',
'sphinx.ext.extlinks', 'sphinx.ext.viewcode'] 'sphinx.ext.extlinks', 'sphinx.ext.viewcode']

View File

@ -33,13 +33,13 @@ User documentation
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
changes
installation/index installation/index
settings settings
running running
clients/index clients/index
authors authors
licenses licenses
changes
Reference documentation Reference documentation
======================= =======================

View File

@ -98,7 +98,7 @@ install Mopidy from PyPI using Pip.
#. Then, you need to install Pip:: #. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian 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:: #. To install the currently latest stable release of Mopidy::
@ -132,7 +132,7 @@ Mopidy's ``develop`` branch.
#. Then, you need to install Pip:: #. Then, you need to install Pip::
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian 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:: #. 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:: #. Then install Git, if haven't already::
sudo aptitude install git-core # On Ubuntu/Debian 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:: #. Clone the official Mopidy repository, or your own fork of it::

View File

@ -2,6 +2,8 @@
:mod:`mopidy.frontends.mpd` -- MPD server :mod:`mopidy.frontends.mpd` -- MPD server
***************************************** *****************************************
.. inheritance-diagram:: mopidy.frontends.mpd
.. automodule:: mopidy.frontends.mpd .. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend :synopsis: MPD frontend
:members: :members:
@ -11,28 +13,30 @@
MPD server MPD server
========== ==========
.. inheritance-diagram:: mopidy.frontends.mpd.server
.. automodule:: mopidy.frontends.mpd.server .. automodule:: mopidy.frontends.mpd.server
:synopsis: MPD server :synopsis: MPD server
:members: :members:
:undoc-members: :undoc-members:
.. inheritance-diagram:: mopidy.frontends.mpd.server
MPD session MPD session
=========== ===========
.. inheritance-diagram:: mopidy.frontends.mpd.session
.. automodule:: mopidy.frontends.mpd.session .. automodule:: mopidy.frontends.mpd.session
:synopsis: MPD client session :synopsis: MPD client session
:members: :members:
:undoc-members: :undoc-members:
.. inheritance-diagram:: mopidy.frontends.mpd.session
MPD dispatcher MPD dispatcher
============== ==============
.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
.. automodule:: mopidy.frontends.mpd.dispatcher .. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher :synopsis: MPD request dispatcher
:members: :members:

View File

@ -6,10 +6,8 @@ The following GStreamer audio outputs implements the :ref:`output-api`.
.. inheritance-diagram:: mopidy.outputs .. 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.shoutcast.ShoutcastOutput
.. autoclass:: mopidy.outputs.ShoutcastOutput

View File

@ -2,6 +2,8 @@ from copy import copy
import logging import logging
import random import random
from mopidy.models import CpTrack
logger = logging.getLogger('mopidy.backends.base') logger = logging.getLogger('mopidy.backends.base')
class CurrentPlaylistController(object): class CurrentPlaylistController(object):
@ -66,7 +68,7 @@ class CurrentPlaylistController(object):
""" """
assert at_position <= len(self._cp_tracks), \ assert at_position <= len(self._cp_tracks), \
u'at_position can not be greater than playlist length' 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: if at_position is not None:
self._cp_tracks.insert(at_position, cp_track) self._cp_tracks.insert(at_position, cp_track)
else: else:

View File

@ -80,12 +80,12 @@ class PlaybackController(object):
def _get_cpid(self, cp_track): def _get_cpid(self, cp_track):
if cp_track is None: if cp_track is None:
return None return None
return cp_track[0] return cp_track.cpid
def _get_track(self, cp_track): def _get_track(self, cp_track):
if cp_track is None: if cp_track is None:
return None return None
return cp_track[1] return cp_track.track
@property @property
def current_cpid(self): def current_cpid(self):
@ -350,7 +350,7 @@ class PlaybackController(object):
self.stop(clear_current_track=True) self.stop(clear_current_track=True)
if self.consume: 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): def on_current_playlist_change(self):
""" """
@ -410,7 +410,7 @@ class PlaybackController(object):
self.stop() self.stop()
self.current_cp_track = cp_track self.current_cp_track = cp_track
self.state = self.PLAYING self.state = self.PLAYING
if not self.provider.play(cp_track[1]): if not self.provider.play(cp_track.track):
# Track is not playable # Track is not playable
if self.random and self._shuffled: if self.random and self._shuffled:
self._shuffled.remove(cp_track) self._shuffled.remove(cp_track)

View File

@ -106,7 +106,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate, 'sample_rate': sample_rate,
'channels': channels, 'channels': channels,
} }
self.gstreamer.deliver_data(capabilites, bytes(frames)) self.gstreamer.emit_data(capabilites, bytes(frames))
def play_token_lost(self, session): def play_token_lost(self, session):
"""Callback used by pyspotify""" """Callback used by pyspotify"""
@ -120,7 +120,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def end_of_track(self, session): def end_of_track(self, session):
"""Callback used by pyspotify""" """Callback used by pyspotify"""
logger.debug(u'End of data stream reached') 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): def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data """Refresh the stored playlists in the backend with fresh meta data

View File

@ -16,28 +16,29 @@ class SpotifyTranslator(object):
return Artist(name=u'[loading...]') return Artist(name=u'[loading...]')
return Artist( return Artist(
uri=str(Link.from_artist(spotify_artist)), uri=str(Link.from_artist(spotify_artist)),
name=spotify_artist.name().decode(ENCODING), name=spotify_artist.name().decode(ENCODING, 'replace'),
) )
@classmethod @classmethod
def to_mopidy_album(cls, spotify_album): 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...]') return Album(name=u'[loading...]')
# TODO pyspotify got much more data on albums than this # 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 @classmethod
def to_mopidy_track(cls, spotify_track): def to_mopidy_track(cls, spotify_track):
uri = str(Link.from_track(spotify_track, 0)) uri = str(Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded(): if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]') 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) date = dt.date(spotify_track.album().year(), 1, 1)
else: else:
date = None date = None
return Track( return Track(
uri=uri, 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()], artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
album=cls.to_mopidy_album(spotify_track.album()), album=cls.to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(), track_no=spotify_track.index(),
@ -56,7 +57,7 @@ class SpotifyTranslator(object):
try: try:
return Playlist( return Playlist(
uri=str(Link.from_playlist(spotify_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 # FIXME if check on link is a hackish workaround for is_local
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist tracks=[cls.to_mopidy_track(t) for t in spotify_playlist
if str(Link.from_track(t, 0))], if str(Link.from_track(t, 0))],

View File

@ -1,10 +1,12 @@
import logging
import re import re
from pykka import ActorDeadError
from pykka.registry import ActorRegistry from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import Backend from mopidy.backends.base import Backend
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, from mopidy.frontends.mpd import exceptions
MpdUnknownCommand)
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
# Do not remove the following import. The protocol modules must be imported to # Do not remove the following import. The protocol modules must be imported to
# get them registered as request handlers. # 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.mixers.base import BaseMixer
from mopidy.utils import flatten from mopidy.utils import flatten
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
class MpdDispatcher(object): class MpdDispatcher(object):
""" """
The MPD session feeds the MPD dispatcher with requests. The dispatcher The MPD session feeds the MPD dispatcher with requests. The dispatcher
@ -23,67 +27,186 @@ class MpdDispatcher(object):
back to the MPD session. back to the MPD session.
""" """
# XXX Consider merging MpdDispatcher into MpdSession def __init__(self, session=None):
self.authenticated = False
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()
self.command_list = False self.command_list = False
self.command_list_ok = 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.""" """Dispatch incoming requests to the correct handler."""
if self.command_list is not False and request != u'command_list_end': self.command_list_index = current_command_list_index
self.command_list.append(request) response = []
return None filter_chain = [
try: self._catch_mpd_ack_errors_filter,
(handler, kwargs) = self.find_handler(request) self._authenticate_filter,
result = handler(self, **kwargs) self._command_list_filter,
except MpdAckError as e: self._add_ok_filter,
if command_list_index is not None: self._call_handler_filter,
e.index = command_list_index ]
return self.handle_response(e.get_mpd_ack(), add_ok=False) return self._call_next_filter(request, response, filter_chain)
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)
def find_handler(self, request): def _call_next_filter(self, request, response, filter_chain):
"""Find the correct handler for a request.""" 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: for pattern in request_handlers:
matches = re.match(pattern, request) matches = re.match(pattern, request)
if matches is not None: if matches is not None:
return (request_handlers[pattern], matches.groupdict()) return (request_handlers[pattern], matches.groupdict())
command = request.split(' ')[0] command_name = request.split(' ')[0]
if command in mpd_commands: if command_name in [command.name for command in mpd_commands]:
raise MpdArgError(u'incorrect arguments', command=command) raise exceptions.MpdArgError(u'incorrect arguments',
raise MpdUnknownCommand(command=command) command=command_name)
raise exceptions.MpdUnknownCommand(command=command_name)
def handle_response(self, result, add_ok=True): def _format_response(self, response):
"""Format the response from a request handler.""" formatted_response = []
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: if result is None:
result = [] return []
elif isinstance(result, set): if isinstance(result, set):
result = list(result) return flatten(list(result))
elif not isinstance(result, list): if not isinstance(result, list):
result = [result] return [result]
for line in flatten(result): return flatten(result)
if isinstance(line, dict):
for (key, value) in line.items(): def _format_lines(self, line):
response.append(u'%s: %s' % (key, value)) if isinstance(line, dict):
elif isinstance(line, tuple): return [u'%s: %s' % (key, value) for (key, value) in line.items()]
(key, value) = line if isinstance(line, tuple):
response.append(u'%s: %s' % (key, value)) (key, value) = line
else: return [u'%s: %s' % (key, value)]
response.append(line) return [line]
if add_ok and (not response or not response[-1].startswith(u'ACK')):
response.append(u'OK')
return response 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

View File

@ -16,10 +16,11 @@ class MpdAckError(MopidyException):
ACK_ERROR_PLAYER_SYNC = 55 ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56 ACK_ERROR_EXIST = 56
def __init__(self, message=u'', error_code=0, index=0, command=u''): error_code = 0
super(MpdAckError, self).__init__(message, error_code, index, command)
def __init__(self, message=u'', index=0, command=u''):
super(MpdAckError, self).__init__(message, index, command)
self.message = message self.message = message
self.error_code = error_code
self.index = index self.index = index
self.command = command self.command = command
@ -30,31 +31,38 @@ class MpdAckError(MopidyException):
ACK [%(error_code)i@%(index)i] {%(command)s} description ACK [%(error_code)i@%(index)i] {%(command)s} description
""" """
return u'ACK [%i@%i] {%s} %s' % ( 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): class MpdArgError(MpdAckError):
def __init__(self, *args, **kwargs): error_code = MpdAckError.ACK_ERROR_ARG
super(MpdArgError, self).__init__(*args, **kwargs)
self.error_code = MpdAckError.ACK_ERROR_ARG
class MpdPasswordError(MpdAckError): class MpdPasswordError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PASSWORD
class MpdPermissionError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PERMISSION
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MpdPasswordError, self).__init__(*args, **kwargs) super(MpdPermissionError, self).__init__(*args, **kwargs)
self.error_code = MpdAckError.ACK_ERROR_PASSWORD self.message = u'you don\'t have permission for "%s"' % self.command
class MpdUnknownCommand(MpdAckError): class MpdUnknownCommand(MpdAckError):
error_code = MpdAckError.ACK_ERROR_UNKNOWN
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs) super(MpdUnknownCommand, self).__init__(*args, **kwargs)
self.message = u'unknown command "%s"' % self.command self.message = u'unknown command "%s"' % self.command
self.command = u'' self.command = u''
self.error_code = MpdAckError.ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError): class MpdNoExistError(MpdAckError):
def __init__(self, *args, **kwargs): error_code = MpdAckError.ACK_ERROR_NO_EXIST
super(MpdNoExistError, self).__init__(*args, **kwargs)
self.error_code = MpdAckError.ACK_ERROR_NO_EXIST class MpdSystemError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_SYSTEM
class MpdNotImplemented(MpdAckError): class MpdNotImplemented(MpdAckError):
error_code = 0
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MpdNotImplemented, self).__init__(*args, **kwargs) super(MpdNotImplemented, self).__init__(*args, **kwargs)
self.message = u'Not implemented' self.message = u'Not implemented'

View File

@ -10,6 +10,7 @@ implement our own MPD server which is compatible with the numerous existing
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_. `MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
""" """
from collections import namedtuple
import re import re
#: The MPD protocol uses UTF-8 for encoding all data. #: 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. #: The MPD protocol version is 0.16.0.
VERSION = u'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() mpd_commands = set()
request_handlers = {} 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 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 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 For example, if the command is ``do that thing`` the ``what`` argument will
be ``this thing``:: be ``this thing``::
@handle_pattern('^do (?P<what>.+)$') @handle_request('^do (?P<what>.+)$')
def do(what): def do(what):
... ...
@ -45,7 +50,8 @@ def handle_pattern(pattern):
def decorator(func): def decorator(func):
match = re.search('([a-z_]+)', pattern) match = re.search('([a-z_]+)', pattern)
if match is not None: 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: if pattern in request_handlers:
raise ValueError(u'Tried to redefine handler for %s with %s' % ( raise ValueError(u'Tried to redefine handler for %s with %s' % (
pattern, func)) pattern, func))

View File

@ -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 from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$') @handle_request(r'^disableoutput "(?P<outputid>\d+)"$')
def disableoutput(frontend, outputid): def disableoutput(context, outputid):
""" """
*musicpd.org, audio output section:* *musicpd.org, audio output section:*
@ -12,8 +12,8 @@ def disableoutput(frontend, outputid):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$') @handle_request(r'^enableoutput "(?P<outputid>\d+)"$')
def enableoutput(frontend, outputid): def enableoutput(context, outputid):
""" """
*musicpd.org, audio output section:* *musicpd.org, audio output section:*
@ -23,8 +23,8 @@ def enableoutput(frontend, outputid):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^outputs$') @handle_request(r'^outputs$')
def outputs(frontend): def outputs(context):
""" """
*musicpd.org, audio output section:* *musicpd.org, audio output section:*

View File

@ -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 from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
@handle_pattern(r'^command_list_begin$') @handle_request(r'^command_list_begin$')
def command_list_begin(frontend): def command_list_begin(context):
""" """
*musicpd.org, command list section:* *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. If ``command_list_ok_begin`` is used, ``list_OK`` is
returned for each successful command executed in the command list. returned for each successful command executed in the command list.
""" """
frontend.command_list = [] context.dispatcher.command_list = []
frontend.command_list_ok = False context.dispatcher.command_list_ok = False
@handle_pattern(r'^command_list_end$') @handle_request(r'^command_list_end$')
def command_list_end(frontend): def command_list_end(context):
"""See :meth:`command_list_begin()`.""" """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 # Test for False exactly, and not e.g. empty list
raise MpdUnknownCommand(command='command_list_end') raise MpdUnknownCommand(command='command_list_end')
(command_list, frontend.command_list) = (frontend.command_list, False) (command_list, context.dispatcher.command_list) = (
(command_list_ok, frontend.command_list_ok) = ( context.dispatcher.command_list, False)
frontend.command_list_ok, False) (command_list_ok, context.dispatcher.command_list_ok) = (
result = [] context.dispatcher.command_list_ok, False)
for i, command in enumerate(command_list): command_list_response = []
response = frontend.handle_request(command, command_list_index=i) for index, command in enumerate(command_list):
if response is not None: response = context.dispatcher.handle_request(
result.append(response) command, current_command_list_index=index)
if response and response[-1].startswith(u'ACK'): command_list_response.extend(response)
return result if (command_list_response and
command_list_response[-1].startswith(u'ACK')):
return command_list_response
if command_list_ok: if command_list_ok:
response.append(u'list_OK') command_list_response.append(u'list_OK')
return result return command_list_response
@handle_pattern(r'^command_list_ok_begin$') @handle_request(r'^command_list_ok_begin$')
def command_list_ok_begin(frontend): def command_list_ok_begin(context):
"""See :meth:`command_list_begin()`.""" """See :meth:`command_list_begin()`."""
frontend.command_list = [] context.dispatcher.command_list = []
frontend.command_list_ok = True context.dispatcher.command_list_ok = True

View File

@ -1,9 +1,10 @@
from mopidy import settings from mopidy import settings
from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdPasswordError from mopidy.frontends.mpd.exceptions import (MpdPasswordError,
MpdPermissionError)
@handle_pattern(r'^close$') @handle_request(r'^close$', auth_required=False)
def close(frontend): def close(context):
""" """
*musicpd.org, connection section:* *musicpd.org, connection section:*
@ -11,10 +12,10 @@ def close(frontend):
Closes the connection to MPD. Closes the connection to MPD.
""" """
pass # TODO context.session.close()
@handle_pattern(r'^kill$') @handle_request(r'^kill$')
def kill(frontend): def kill(context):
""" """
*musicpd.org, connection section:* *musicpd.org, connection section:*
@ -22,10 +23,10 @@ def kill(frontend):
Kills MPD. Kills MPD.
""" """
pass # TODO raise MpdPermissionError(command=u'kill')
@handle_pattern(r'^password "(?P<password>[^"]+)"$') @handle_request(r'^password "(?P<password>[^"]+)"$', auth_required=False)
def password_(frontend, password): def password_(context, password):
""" """
*musicpd.org, connection section:* *musicpd.org, connection section:*
@ -34,14 +35,13 @@ def password_(frontend, password):
This is used for authentication with the server. ``PASSWORD`` is This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password. simply the plaintext password.
""" """
# You will not get to this code without being authenticated. This is for if password == settings.MPD_SERVER_PASSWORD:
# when you are already authenticated, and are sending additional 'password' context.dispatcher.authenticated = True
# requests. else:
if settings.MPD_SERVER_PASSWORD != password:
raise MpdPasswordError(u'incorrect password', command=u'password') raise MpdPasswordError(u'incorrect password', command=u'password')
@handle_pattern(r'^ping$') @handle_request(r'^ping$', auth_required=False)
def ping(frontend): def ping(context):
""" """
*musicpd.org, connection section:* *musicpd.org, connection section:*

View File

@ -1,10 +1,10 @@
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented) 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 from mopidy.frontends.mpd.translator import tracks_to_mpd_format
@handle_pattern(r'^add "(?P<uri>[^"]*)"$') @handle_request(r'^add "(?P<uri>[^"]*)"$')
def add(frontend, uri): def add(context, uri):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -19,17 +19,17 @@ def add(frontend, uri):
""" """
if not uri: if not uri:
return return
for handler_prefix in frontend.backend.uri_handlers.get(): for handler_prefix in context.backend.uri_handlers.get():
if uri.startswith(handler_prefix): if uri.startswith(handler_prefix):
track = frontend.backend.library.lookup(uri).get() track = context.backend.library.lookup(uri).get()
if track is not None: if track is not None:
frontend.backend.current_playlist.add(track) context.backend.current_playlist.add(track)
return return
raise MpdNoExistError( raise MpdNoExistError(
u'directory or file not found', command=u'add') u'directory or file not found', command=u'add')
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$') @handle_request(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
def addid(frontend, uri, songpos=None): def addid(context, uri, songpos=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -51,18 +51,18 @@ def addid(frontend, uri, songpos=None):
raise MpdNoExistError(u'No such song', command=u'addid') raise MpdNoExistError(u'No such song', command=u'addid')
if songpos is not None: if songpos is not None:
songpos = int(songpos) songpos = int(songpos)
track = frontend.backend.library.lookup(uri).get() track = context.backend.library.lookup(uri).get()
if track is None: if track is None:
raise MpdNoExistError(u'No such song', command=u'addid') raise MpdNoExistError(u'No such song', command=u'addid')
if songpos and songpos > len( 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') 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() at_position=songpos).get()
return ('Id', cp_track[0]) return ('Id', cp_track.cpid)
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$') @handle_request(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
def delete_range(frontend, start, end=None): def delete_range(context, start, end=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -74,25 +74,25 @@ def delete_range(frontend, start, end=None):
if end is not None: if end is not None:
end = int(end) end = int(end)
else: else:
end = len(frontend.backend.current_playlist.tracks.get()) end = len(context.backend.current_playlist.tracks.get())
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end] cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end]
if not cp_tracks: if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete') raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks: for (cpid, _) in cp_tracks:
frontend.backend.current_playlist.remove(cpid=cpid) context.backend.current_playlist.remove(cpid=cpid)
@handle_pattern(r'^delete "(?P<songpos>\d+)"$') @handle_request(r'^delete "(?P<songpos>\d+)"$')
def delete_songpos(frontend, songpos): def delete_songpos(context, songpos):
"""See :meth:`delete_range`""" """See :meth:`delete_range`"""
try: try:
songpos = int(songpos) songpos = int(songpos)
(cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos] (cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos]
frontend.backend.current_playlist.remove(cpid=cpid) context.backend.current_playlist.remove(cpid=cpid)
except IndexError: except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete') raise MpdArgError(u'Bad song index', command=u'delete')
@handle_pattern(r'^deleteid "(?P<cpid>\d+)"$') @handle_request(r'^deleteid "(?P<cpid>\d+)"$')
def deleteid(frontend, cpid): def deleteid(context, cpid):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -102,14 +102,14 @@ def deleteid(frontend, cpid):
""" """
try: try:
cpid = int(cpid) cpid = int(cpid)
if frontend.backend.playback.current_cpid.get() == cpid: if context.backend.playback.current_cpid.get() == cpid:
frontend.backend.playback.next() context.backend.playback.next()
return frontend.backend.current_playlist.remove(cpid=cpid).get() return context.backend.current_playlist.remove(cpid=cpid).get()
except LookupError: except LookupError:
raise MpdNoExistError(u'No such song', command=u'deleteid') raise MpdNoExistError(u'No such song', command=u'deleteid')
@handle_pattern(r'^clear$') @handle_request(r'^clear$')
def clear(frontend): def clear(context):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -117,10 +117,10 @@ def clear(frontend):
Clears the current playlist. Clears the current playlist.
""" """
frontend.backend.current_playlist.clear() context.backend.current_playlist.clear()
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$') @handle_request(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
def move_range(frontend, start, to, end=None): def move_range(context, start, to, end=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -130,21 +130,21 @@ def move_range(frontend, start, to, end=None):
``TO`` in the playlist. ``TO`` in the playlist.
""" """
if end is None: if end is None:
end = len(frontend.backend.current_playlist.tracks.get()) end = len(context.backend.current_playlist.tracks.get())
start = int(start) start = int(start)
end = int(end) end = int(end)
to = int(to) to = int(to)
frontend.backend.current_playlist.move(start, end, to) context.backend.current_playlist.move(start, end, to)
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$') @handle_request(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
def move_songpos(frontend, songpos, to): def move_songpos(context, songpos, to):
"""See :meth:`move_range`.""" """See :meth:`move_range`."""
songpos = int(songpos) songpos = int(songpos)
to = int(to) 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<cpid>\d+)" "(?P<to>\d+)"$') @handle_request(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
def moveid(frontend, cpid, to): def moveid(context, cpid, to):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -156,13 +156,13 @@ def moveid(frontend, cpid, to):
""" """
cpid = int(cpid) cpid = int(cpid)
to = int(to) to = int(to)
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() cp_track = context.backend.current_playlist.get(cpid=cpid).get()
position = frontend.backend.current_playlist.cp_tracks.get().index( position = context.backend.current_playlist.cp_tracks.get().index(
cp_track) cp_track)
frontend.backend.current_playlist.move(position, position + 1, to) context.backend.current_playlist.move(position, position + 1, to)
@handle_pattern(r'^playlist$') @handle_request(r'^playlist$')
def playlist(frontend): def playlist(context):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -174,11 +174,11 @@ def playlist(frontend):
Do not use this, instead use ``playlistinfo``. Do not use this, instead use ``playlistinfo``.
""" """
return playlistinfo(frontend) return playlistinfo(context)
@handle_pattern(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$') @handle_request(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$') @handle_request(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
def playlistfind(frontend, tag, needle): def playlistfind(context, tag, needle):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -192,17 +192,17 @@ def playlistfind(frontend, tag, needle):
""" """
if tag == 'filename': if tag == 'filename':
try: 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 (cpid, track) = cp_track
position = frontend.backend.current_playlist.cp_tracks.get().index( position = context.backend.current_playlist.cp_tracks.get().index(
cp_track) cp_track)
return track.mpd_format(cpid=cpid, position=position) return track.mpd_format(cpid=cpid, position=position)
except LookupError: except LookupError:
return None return None
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^playlistid( "(?P<cpid>\d+)")*$') @handle_request(r'^playlistid( "(?P<cpid>\d+)")*$')
def playlistid(frontend, cpid=None): def playlistid(context, cpid=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -214,22 +214,22 @@ def playlistid(frontend, cpid=None):
if cpid is not None: if cpid is not None:
try: try:
cpid = int(cpid) cpid = int(cpid)
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() cp_track = context.backend.current_playlist.get(cpid=cpid).get()
position = frontend.backend.current_playlist.cp_tracks.get().index( position = context.backend.current_playlist.cp_tracks.get().index(
cp_track) cp_track)
return cp_track[1].mpd_format(position=position, cpid=cpid) return cp_track.track.mpd_format(position=position, cpid=cpid)
except LookupError: except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid') raise MpdNoExistError(u'No such song', command=u'playlistid')
else: else:
cpids = [ct[0] for ct in 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( 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_request(r'^playlistinfo$')
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$') @handle_request(r'^playlistinfo "(?P<songpos>-?\d+)"$')
@handle_pattern(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$') @handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
def playlistinfo(frontend, songpos=None, def playlistinfo(context, songpos=None,
start=None, end=None): start=None, end=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -255,30 +255,30 @@ def playlistinfo(frontend, songpos=None,
if start == -1: if start == -1:
end = None end = None
cpids = [ct[0] for ct in 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( return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(), context.backend.current_playlist.tracks.get(),
start, end, cpids=cpids) start, end, cpids=cpids)
else: else:
if start is None: if start is None:
start = 0 start = 0
start = int(start) start = int(start)
if not (0 <= start <= len( 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') raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None: if end is not None:
end = int(end) end = int(end)
if end > len(frontend.backend.current_playlist.tracks.get()): if end > len(context.backend.current_playlist.tracks.get()):
end = None end = None
cpids = [ct[0] for ct in 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( return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(), context.backend.current_playlist.tracks.get(),
start, end, cpids=cpids) start, end, cpids=cpids)
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$') @handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$') @handle_request(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
def playlistsearch(frontend, tag, needle): def playlistsearch(context, tag, needle):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -294,9 +294,9 @@ def playlistsearch(frontend, tag, needle):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^plchanges (?P<version>-?\d+)$') @handle_request(r'^plchanges (?P<version>-?\d+)$')
@handle_pattern(r'^plchanges "(?P<version>-?\d+)"$') @handle_request(r'^plchanges "(?P<version>-?\d+)"$')
def plchanges(frontend, version): def plchanges(context, version):
""" """
*musicpd.org, current playlist section:* *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. - Calls ``plchanges "-1"`` two times per second to get the entire playlist.
""" """
# XXX Naive implementation that returns all tracks as changed # 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 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( 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<version>\d+)"$') @handle_request(r'^plchangesposid "(?P<version>\d+)"$')
def plchangesposid(frontend, version): def plchangesposid(context, version):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -333,17 +333,17 @@ def plchangesposid(frontend, version):
``playlistlength`` returned by status command. ``playlistlength`` returned by status command.
""" """
# XXX Naive implementation that returns all tracks as changed # 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 = [] result = []
for (position, (cpid, _)) in enumerate( 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'cpos', position))
result.append((u'Id', cpid)) result.append((u'Id', cpid))
return result return result
@handle_pattern(r'^shuffle$') @handle_request(r'^shuffle$')
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$') @handle_request(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
def shuffle(frontend, start=None, end=None): def shuffle(context, start=None, end=None):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -356,10 +356,10 @@ def shuffle(frontend, start=None, end=None):
start = int(start) start = int(start)
if end is not None: if end is not None:
end = int(end) end = int(end)
frontend.backend.current_playlist.shuffle(start, end) context.backend.current_playlist.shuffle(start, end)
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$') @handle_request(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
def swap(frontend, songpos1, songpos2): def swap(context, songpos1, songpos2):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -369,18 +369,18 @@ def swap(frontend, songpos1, songpos2):
""" """
songpos1 = int(songpos1) songpos1 = int(songpos1)
songpos2 = int(songpos2) songpos2 = int(songpos2)
tracks = frontend.backend.current_playlist.tracks.get() tracks = context.backend.current_playlist.tracks.get()
song1 = tracks[songpos1] song1 = tracks[songpos1]
song2 = tracks[songpos2] song2 = tracks[songpos2]
del tracks[songpos1] del tracks[songpos1]
tracks.insert(songpos1, song2) tracks.insert(songpos1, song2)
del tracks[songpos2] del tracks[songpos2]
tracks.insert(songpos2, song1) tracks.insert(songpos2, song1)
frontend.backend.current_playlist.clear() context.backend.current_playlist.clear()
frontend.backend.current_playlist.append(tracks) context.backend.current_playlist.append(tracks)
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$') @handle_request(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
def swapid(frontend, cpid1, cpid2): def swapid(context, cpid1, cpid2):
""" """
*musicpd.org, current playlist section:* *musicpd.org, current playlist section:*
@ -390,9 +390,9 @@ def swapid(frontend, cpid1, cpid2):
""" """
cpid1 = int(cpid1) cpid1 = int(cpid1)
cpid2 = int(cpid2) cpid2 = int(cpid2)
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get() cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get() cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
cp_tracks = frontend.backend.current_playlist.cp_tracks.get() cp_tracks = context.backend.current_playlist.cp_tracks.get()
position1 = cp_tracks.index(cp_track1) position1 = cp_tracks.index(cp_track1)
position2 = cp_tracks.index(cp_track2) position2 = cp_tracks.index(cp_track2)
swap(frontend, position1, position2) swap(context, position1, position2)

View File

@ -1,6 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.protocol import handle_request
@handle_pattern(r'^$') @handle_request(r'^$')
def empty(frontend): def empty(context):
"""The original MPD server returns ``OK`` on an empty request.""" """The original MPD server returns ``OK`` on an empty request."""
pass pass

View File

@ -1,7 +1,7 @@
import re import re
import shlex 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 from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
def _build_query(mpd_query): def _build_query(mpd_query):
@ -28,8 +28,8 @@ def _build_query(mpd_query):
query[field] = [what] query[field] = [what]
return query return query
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$') @handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
def count(frontend, tag, needle): def count(context, tag, needle):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -40,10 +40,10 @@ def count(frontend, tag, needle):
""" """
return [('songs', 0), ('playtime', 0)] # TODO return [('songs', 0), ('playtime', 0)] # TODO
@handle_pattern(r'^find ' @handle_request(r'^find '
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|' r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def find(frontend, mpd_query): def find(context, mpd_query):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -68,12 +68,12 @@ def find(frontend, mpd_query):
- also uses the search type "date". - also uses the search type "date".
""" """
query = _build_query(mpd_query) 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<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
'"[^"]+"\s?)+)$') '"[^"]+"\s?)+)$')
def findadd(frontend, query): def findadd(context, query):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -84,11 +84,11 @@ def findadd(frontend, query):
``WHAT`` is what to find. ``WHAT`` is what to find.
""" """
# TODO Add result to current playlist # TODO Add result to current playlist
#result = frontend.find(query) #result = context.find(query)
@handle_pattern(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?' @handle_request(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
'( (?P<mpd_query>.*))?$') '( (?P<mpd_query>.*))?$')
def list_(frontend, field, mpd_query=None): def list_(context, field, mpd_query=None):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -175,11 +175,11 @@ def list_(frontend, field, mpd_query=None):
field = field.lower() field = field.lower()
query = _list_build_query(field, mpd_query) query = _list_build_query(field, mpd_query)
if field == u'artist': if field == u'artist':
return _list_artist(frontend, query) return _list_artist(context, query)
elif field == u'album': elif field == u'album':
return _list_album(frontend, query) return _list_album(context, query)
elif field == u'date': elif field == u'date':
return _list_date(frontend, query) return _list_date(context, query)
elif field == u'genre': elif field == u'genre':
pass # TODO We don't have genre in our internal data structures yet 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: else:
raise MpdArgError(u'not able to parse args', command=u'list') raise MpdArgError(u'not able to parse args', command=u'list')
def _list_artist(frontend, query): def _list_artist(context, query):
artists = set() artists = set()
playlist = frontend.backend.library.find_exact(**query).get() playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks: for track in playlist.tracks:
for artist in track.artists: for artist in track.artists:
artists.add((u'Artist', artist.name)) artists.add((u'Artist', artist.name))
return artists return artists
def _list_album(frontend, query): def _list_album(context, query):
albums = set() albums = set()
playlist = frontend.backend.library.find_exact(**query).get() playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks: for track in playlist.tracks:
if track.album is not None: if track.album is not None:
albums.add((u'Album', track.album.name)) albums.add((u'Album', track.album.name))
return albums return albums
def _list_date(frontend, query): def _list_date(context, query):
dates = set() dates = set()
playlist = frontend.backend.library.find_exact(**query).get() playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks: for track in playlist.tracks:
if track.date is not None: if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d'))) dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
return dates return dates
@handle_pattern(r'^listall "(?P<uri>[^"]+)"') @handle_request(r'^listall "(?P<uri>[^"]+)"')
def listall(frontend, uri): def listall(context, uri):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -248,8 +248,8 @@ def listall(frontend, uri):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^listallinfo "(?P<uri>[^"]+)"') @handle_request(r'^listallinfo "(?P<uri>[^"]+)"')
def listallinfo(frontend, uri): def listallinfo(context, uri):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -260,9 +260,9 @@ def listallinfo(frontend, uri):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^lsinfo$') @handle_request(r'^lsinfo$')
@handle_pattern(r'^lsinfo "(?P<uri>[^"]*)"$') @handle_request(r'^lsinfo "(?P<uri>[^"]*)"$')
def lsinfo(frontend, uri=None): def lsinfo(context, uri=None):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -279,11 +279,11 @@ def lsinfo(frontend, uri=None):
""``, and ``lsinfo "/"``. ""``, and ``lsinfo "/"``.
""" """
if uri is None or uri == u'/' or uri == u'': if uri is None or uri == u'/' or uri == u'':
return stored_playlists.listplaylists(frontend) return stored_playlists.listplaylists(context)
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$') @handle_request(r'^rescan( "(?P<uri>[^"]+)")*$')
def rescan(frontend, uri=None): def rescan(context, uri=None):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -291,12 +291,12 @@ def rescan(frontend, uri=None):
Same as ``update``, but also rescans unmodified files. 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<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|' r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def search(frontend, mpd_query): def search(context, mpd_query):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*
@ -324,10 +324,10 @@ def search(frontend, mpd_query):
- also uses the search type "date". - also uses the search type "date".
""" """
query = _build_query(mpd_query) 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<uri>[^"]+)")*$') @handle_request(r'^update( "(?P<uri>[^"]+)")*$')
def update(frontend, uri=None, rescan_unmodified_files=False): def update(context, uri=None, rescan_unmodified_files=False):
""" """
*musicpd.org, music database section:* *musicpd.org, music database section:*

View File

@ -1,11 +1,11 @@
from mopidy.backends.base import PlaybackController 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, from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented) MpdNotImplemented)
@handle_pattern(r'^consume (?P<state>[01])$') @handle_request(r'^consume (?P<state>[01])$')
@handle_pattern(r'^consume "(?P<state>[01])"$') @handle_request(r'^consume "(?P<state>[01])"$')
def consume(frontend, state): def consume(context, state):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -16,12 +16,12 @@ def consume(frontend, state):
playlist. playlist.
""" """
if int(state): if int(state):
frontend.backend.playback.consume = True context.backend.playback.consume = True
else: else:
frontend.backend.playback.consume = False context.backend.playback.consume = False
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$') @handle_request(r'^crossfade "(?P<seconds>\d+)"$')
def crossfade(frontend, seconds): def crossfade(context, seconds):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -32,8 +32,8 @@ def crossfade(frontend, seconds):
seconds = int(seconds) seconds = int(seconds)
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^next$') @handle_request(r'^next$')
def next_(frontend): def next_(context):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -87,11 +87,11 @@ def next_(frontend):
order as the first time. order as the first time.
""" """
return frontend.backend.playback.next().get() return context.backend.playback.next().get()
@handle_pattern(r'^pause$') @handle_request(r'^pause$')
@handle_pattern(r'^pause "(?P<state>[01])"$') @handle_request(r'^pause "(?P<state>[01])"$')
def pause(frontend, state=None): def pause(context, state=None):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -104,28 +104,28 @@ def pause(frontend, state=None):
- Calls ``pause`` without any arguments to toogle pause. - Calls ``pause`` without any arguments to toogle pause.
""" """
if state is None: if state is None:
if (frontend.backend.playback.state.get() == if (context.backend.playback.state.get() ==
PlaybackController.PLAYING): PlaybackController.PLAYING):
frontend.backend.playback.pause() context.backend.playback.pause()
elif (frontend.backend.playback.state.get() == elif (context.backend.playback.state.get() ==
PlaybackController.PAUSED): PlaybackController.PAUSED):
frontend.backend.playback.resume() context.backend.playback.resume()
elif int(state): elif int(state):
frontend.backend.playback.pause() context.backend.playback.pause()
else: else:
frontend.backend.playback.resume() context.backend.playback.resume()
@handle_pattern(r'^play$') @handle_request(r'^play$')
def play(frontend): def play(context):
""" """
The original MPD server resumes from the paused state on ``play`` The original MPD server resumes from the paused state on ``play``
without arguments. without arguments.
""" """
return frontend.backend.playback.play().get() return context.backend.playback.play().get()
@handle_pattern(r'^playid "(?P<cpid>\d+)"$') @handle_request(r'^playid "(?P<cpid>\d+)"$')
@handle_pattern(r'^playid "(?P<cpid>-1)"$') @handle_request(r'^playid "(?P<cpid>-1)"$')
def playid(frontend, cpid): def playid(context, cpid):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -144,16 +144,16 @@ def playid(frontend, cpid):
""" """
cpid = int(cpid) cpid = int(cpid)
if cpid == -1: if cpid == -1:
return _play_minus_one(frontend) return _play_minus_one(context)
try: try:
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() cp_track = context.backend.current_playlist.get(cpid=cpid).get()
return frontend.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
except LookupError: except LookupError:
raise MpdNoExistError(u'No such song', command=u'playid') raise MpdNoExistError(u'No such song', command=u'playid')
@handle_pattern(r'^play (?P<songpos>-?\d+)$') @handle_request(r'^play (?P<songpos>-?\d+)$')
@handle_pattern(r'^play "(?P<songpos>-?\d+)"$') @handle_request(r'^play "(?P<songpos>-?\d+)"$')
def playpos(frontend, songpos): def playpos(context, songpos):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -176,29 +176,29 @@ def playpos(frontend, songpos):
""" """
songpos = int(songpos) songpos = int(songpos)
if songpos == -1: if songpos == -1:
return _play_minus_one(frontend) return _play_minus_one(context)
try: try:
cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos] cp_track = context.backend.current_playlist.cp_tracks.get()[songpos]
return frontend.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
except IndexError: except IndexError:
raise MpdArgError(u'Bad song index', command=u'play') raise MpdArgError(u'Bad song index', command=u'play')
def _play_minus_one(frontend): def _play_minus_one(context):
if (frontend.backend.playback.state.get() == PlaybackController.PLAYING): if (context.backend.playback.state.get() == PlaybackController.PLAYING):
return # Nothing to do return # Nothing to do
elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED): elif (context.backend.playback.state.get() == PlaybackController.PAUSED):
return frontend.backend.playback.resume().get() return context.backend.playback.resume().get()
elif frontend.backend.playback.current_cp_track.get() is not None: elif context.backend.playback.current_cp_track.get() is not None:
cp_track = frontend.backend.playback.current_cp_track.get() cp_track = context.backend.playback.current_cp_track.get()
return frontend.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
elif frontend.backend.current_playlist.cp_tracks.get(): elif context.backend.current_playlist.cp_tracks.get():
cp_track = frontend.backend.current_playlist.cp_tracks.get()[0] cp_track = context.backend.current_playlist.cp_tracks.get()[0]
return frontend.backend.playback.play(cp_track).get() return context.backend.playback.play(cp_track).get()
else: else:
return # Fail silently return # Fail silently
@handle_pattern(r'^previous$') @handle_request(r'^previous$')
def previous(frontend): def previous(context):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -241,11 +241,11 @@ def previous(frontend):
``previous`` should do a seek to time position 0. ``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<state>[01])$') @handle_request(r'^random (?P<state>[01])$')
@handle_pattern(r'^random "(?P<state>[01])"$') @handle_request(r'^random "(?P<state>[01])"$')
def random(frontend, state): def random(context, state):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -254,13 +254,13 @@ def random(frontend, state):
Sets random state to ``STATE``, ``STATE`` should be 0 or 1. Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
""" """
if int(state): if int(state):
frontend.backend.playback.random = True context.backend.playback.random = True
else: else:
frontend.backend.playback.random = False context.backend.playback.random = False
@handle_pattern(r'^repeat (?P<state>[01])$') @handle_request(r'^repeat (?P<state>[01])$')
@handle_pattern(r'^repeat "(?P<state>[01])"$') @handle_request(r'^repeat "(?P<state>[01])"$')
def repeat(frontend, state): def repeat(context, state):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -269,12 +269,12 @@ def repeat(frontend, state):
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1. Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
""" """
if int(state): if int(state):
frontend.backend.playback.repeat = True context.backend.playback.repeat = True
else: else:
frontend.backend.playback.repeat = False context.backend.playback.repeat = False
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$') @handle_request(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
def replay_gain_mode(frontend, mode): def replay_gain_mode(context, mode):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -289,8 +289,8 @@ def replay_gain_mode(frontend, mode):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^replay_gain_status$') @handle_request(r'^replay_gain_status$')
def replay_gain_status(frontend): def replay_gain_status(context):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -301,9 +301,9 @@ def replay_gain_status(frontend):
""" """
return u'off' # TODO return u'off' # TODO
@handle_pattern(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$') @handle_request(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$')
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$') @handle_request(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
def seek(frontend, songpos, seconds): def seek(context, songpos, seconds):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -316,12 +316,12 @@ def seek(frontend, songpos, seconds):
- issues ``seek 1 120`` without quotes around the arguments. - issues ``seek 1 120`` without quotes around the arguments.
""" """
if frontend.backend.playback.current_playlist_position != songpos: if context.backend.playback.current_playlist_position != songpos:
playpos(frontend, songpos) playpos(context, songpos)
frontend.backend.playback.seek(int(seconds) * 1000) context.backend.playback.seek(int(seconds) * 1000)
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$') @handle_request(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
def seekid(frontend, cpid, seconds): def seekid(context, cpid, seconds):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -329,13 +329,13 @@ def seekid(frontend, cpid, seconds):
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``. Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
""" """
if frontend.backend.playback.current_cpid != cpid: if context.backend.playback.current_cpid != cpid:
playid(frontend, cpid) playid(context, cpid)
frontend.backend.playback.seek(int(seconds) * 1000) context.backend.playback.seek(int(seconds) * 1000)
@handle_pattern(r'^setvol (?P<volume>[-+]*\d+)$') @handle_request(r'^setvol (?P<volume>[-+]*\d+)$')
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$') @handle_request(r'^setvol "(?P<volume>[-+]*\d+)"$')
def setvol(frontend, volume): def setvol(context, volume):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -352,11 +352,11 @@ def setvol(frontend, volume):
volume = 0 volume = 0
if volume > 100: if volume > 100:
volume = 100 volume = 100
frontend.mixer.volume = volume context.mixer.volume = volume
@handle_pattern(r'^single (?P<state>[01])$') @handle_request(r'^single (?P<state>[01])$')
@handle_pattern(r'^single "(?P<state>[01])"$') @handle_request(r'^single "(?P<state>[01])"$')
def single(frontend, state): def single(context, state):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -367,12 +367,12 @@ def single(frontend, state):
song is repeated if the ``repeat`` mode is enabled. song is repeated if the ``repeat`` mode is enabled.
""" """
if int(state): if int(state):
frontend.backend.playback.single = True context.backend.playback.single = True
else: else:
frontend.backend.playback.single = False context.backend.playback.single = False
@handle_pattern(r'^stop$') @handle_request(r'^stop$')
def stop(frontend): def stop(context):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -380,4 +380,4 @@ def stop(frontend):
Stops playing. Stops playing.
""" """
frontend.backend.playback.stop() context.backend.playback.stop()

View File

@ -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 from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^commands$') @handle_request(r'^commands$', auth_required=False)
def commands(frontend): def commands(context):
""" """
*musicpd.org, reflection section:* *musicpd.org, reflection section:*
@ -10,25 +10,34 @@ def commands(frontend):
Shows which commands the current user has access to. Shows which commands the current user has access to.
""" """
# FIXME When password auth is turned on and the client is not if context.dispatcher.authenticated:
# authenticated, 'commands' should list only the commands the client does command_names = [command.name for command in mpd_commands]
# have access to. To implement this we need access to the session object to else:
# check if the client is authenticated or not. 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 # Not shown by MPD in its command list
sorted_commands.remove('command_list_begin') if 'command_list_begin' in command_names:
sorted_commands.remove('command_list_ok_begin') command_names.remove('command_list_begin')
sorted_commands.remove('command_list_end') if 'command_list_ok_begin' in command_names:
sorted_commands.remove('idle') command_names.remove('command_list_ok_begin')
sorted_commands.remove('noidle') if 'command_list_end' in command_names:
sorted_commands.remove('sticker') 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$') @handle_request(r'^decoders$')
def decoders(frontend): def decoders(context):
""" """
*musicpd.org, reflection section:* *musicpd.org, reflection section:*
@ -46,8 +55,8 @@ def decoders(frontend):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^notcommands$') @handle_request(r'^notcommands$', auth_required=False)
def notcommands(frontend): def notcommands(context):
""" """
*musicpd.org, reflection section:* *musicpd.org, reflection section:*
@ -55,14 +64,19 @@ def notcommands(frontend):
Shows which commands the current user does not have access to. Shows which commands the current user does not have access to.
""" """
# FIXME When password auth is turned on and the client is not if context.dispatcher.authenticated:
# authenticated, 'notcommands' should list all the commands the client does command_names = []
# not have access to. To implement this we need access to the session else:
# object to check if the client is authenticated or not. command_names = [command.name for command in mpd_commands
pass if command.auth_required]
@handle_pattern(r'^tagtypes$') # No permission to use
def tagtypes(frontend): 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:* *musicpd.org, reflection section:*
@ -72,8 +86,8 @@ def tagtypes(frontend):
""" """
pass # TODO pass # TODO
@handle_pattern(r'^urlhandlers$') @handle_request(r'^urlhandlers$')
def urlhandlers(frontend): def urlhandlers(context):
""" """
*musicpd.org, reflection section:* *musicpd.org, reflection section:*
@ -81,4 +95,4 @@ def urlhandlers(frontend):
Gets a list of available URL handlers. 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()]

View File

@ -1,9 +1,11 @@
import pykka.future
from mopidy.backends.base import PlaybackController 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 from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^clearerror$') @handle_request(r'^clearerror$')
def clearerror(frontend): def clearerror(context):
""" """
*musicpd.org, status section:* *musicpd.org, status section:*
@ -14,8 +16,8 @@ def clearerror(frontend):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^currentsong$') @handle_request(r'^currentsong$')
def currentsong(frontend): def currentsong(context):
""" """
*musicpd.org, status section:* *musicpd.org, status section:*
@ -24,15 +26,15 @@ def currentsong(frontend):
Displays the song info of the current song (same song that is Displays the song info of the current song (same song that is
identified in status). 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: if current_cp_track is not None:
return current_cp_track[1].mpd_format( return current_cp_track.track.mpd_format(
position=frontend.backend.playback.current_playlist_position.get(), position=context.backend.playback.current_playlist_position.get(),
cpid=current_cp_track[0]) cpid=current_cp_track.cpid)
@handle_pattern(r'^idle$') @handle_request(r'^idle$')
@handle_pattern(r'^idle (?P<subsystems>.+)$') @handle_request(r'^idle (?P<subsystems>.+)$')
def idle(frontend, subsystems=None): def idle(context, subsystems=None):
""" """
*musicpd.org, status section:* *musicpd.org, status section:*
@ -67,13 +69,13 @@ def idle(frontend, subsystems=None):
""" """
pass # TODO pass # TODO
@handle_pattern(r'^noidle$') @handle_request(r'^noidle$')
def noidle(frontend): def noidle(context):
"""See :meth:`_status_idle`.""" """See :meth:`_status_idle`."""
pass # TODO pass # TODO
@handle_pattern(r'^stats$') @handle_request(r'^stats$')
def stats(frontend): def stats(context):
""" """
*musicpd.org, status section:* *musicpd.org, status section:*
@ -98,8 +100,8 @@ def stats(frontend):
'playtime': 0, # TODO 'playtime': 0, # TODO
} }
@handle_pattern(r'^status$') @handle_request(r'^status$')
def status(frontend): def status(context):
""" """
*musicpd.org, status section:* *musicpd.org, status section:*
@ -130,65 +132,80 @@ def status(frontend):
- ``updatings_db``: job id - ``updatings_db``: job id
- ``error``: if there is an error, returns message here - ``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 = [ result = [
('volume', _status_volume(frontend)), ('volume', _status_volume(futures)),
('repeat', _status_repeat(frontend)), ('repeat', _status_repeat(futures)),
('random', _status_random(frontend)), ('random', _status_random(futures)),
('single', _status_single(frontend)), ('single', _status_single(futures)),
('consume', _status_consume(frontend)), ('consume', _status_consume(futures)),
('playlist', _status_playlist_version(frontend)), ('playlist', _status_playlist_version(futures)),
('playlistlength', _status_playlist_length(frontend)), ('playlistlength', _status_playlist_length(futures)),
('xfade', _status_xfade(frontend)), ('xfade', _status_xfade(futures)),
('state', _status_state(frontend)), ('state', _status_state(futures)),
] ]
if frontend.backend.playback.current_track.get() is not None: if futures['playback.current_cp_track'].get() is not None:
result.append(('song', _status_songpos(frontend))) result.append(('song', _status_songpos(futures)))
result.append(('songid', _status_songid(frontend))) result.append(('songid', _status_songid(futures)))
if frontend.backend.playback.state.get() in (PlaybackController.PLAYING, if futures['playback.state'].get() in (PlaybackController.PLAYING,
PlaybackController.PAUSED): PlaybackController.PAUSED):
result.append(('time', _status_time(frontend))) result.append(('time', _status_time(futures)))
result.append(('elapsed', _status_time_elapsed(frontend))) result.append(('elapsed', _status_time_elapsed(futures)))
result.append(('bitrate', _status_bitrate(frontend))) result.append(('bitrate', _status_bitrate(futures)))
return result return result
def _status_bitrate(frontend): def _status_bitrate(futures):
current_track = frontend.backend.playback.current_track.get() current_cp_track = futures['playback.current_cp_track'].get()
if current_track is not None: if current_cp_track is not None:
return current_track.bitrate return current_cp_track.track.bitrate
def _status_consume(frontend): def _status_consume(futures):
if frontend.backend.playback.consume.get(): if futures['playback.consume'].get():
return 1 return 1
else: else:
return 0 return 0
def _status_playlist_length(frontend): def _status_playlist_length(futures):
return len(frontend.backend.current_playlist.tracks.get()) return len(futures['current_playlist.tracks'].get())
def _status_playlist_version(frontend): def _status_playlist_version(futures):
return frontend.backend.current_playlist.version.get() return futures['current_playlist.version'].get()
def _status_random(frontend): def _status_random(futures):
return int(frontend.backend.playback.random.get()) return int(futures['playback.random'].get())
def _status_repeat(frontend): def _status_repeat(futures):
return int(frontend.backend.playback.repeat.get()) return int(futures['playback.repeat'].get())
def _status_single(frontend): def _status_single(futures):
return int(frontend.backend.playback.single.get()) return int(futures['playback.single'].get())
def _status_songid(frontend): def _status_songid(futures):
current_cpid = frontend.backend.playback.current_cpid.get() current_cp_track = futures['playback.current_cp_track'].get()
if current_cpid is not None: if current_cp_track is not None:
return current_cpid return current_cp_track.cpid
else: else:
return _status_songpos(frontend) return _status_songpos(futures)
def _status_songpos(frontend): def _status_songpos(futures):
return frontend.backend.playback.current_playlist_position.get() return futures['playback.current_playlist_position'].get()
def _status_state(frontend): def _status_state(futures):
state = frontend.backend.playback.state.get() state = futures['playback.state'].get()
if state == PlaybackController.PLAYING: if state == PlaybackController.PLAYING:
return u'play' return u'play'
elif state == PlaybackController.STOPPED: elif state == PlaybackController.STOPPED:
@ -196,28 +213,28 @@ def _status_state(frontend):
elif state == PlaybackController.PAUSED: elif state == PlaybackController.PAUSED:
return u'pause' return u'pause'
def _status_time(frontend): def _status_time(futures):
return u'%s:%s' % (_status_time_elapsed(frontend) // 1000, return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
_status_time_total(frontend) // 1000) _status_time_total(futures) // 1000)
def _status_time_elapsed(frontend): def _status_time_elapsed(futures):
return frontend.backend.playback.time_position.get() return futures['playback.time_position'].get()
def _status_time_total(frontend): def _status_time_total(futures):
current_track = frontend.backend.playback.current_track.get() current_cp_track = futures['playback.current_cp_track'].get()
if current_track is None: if current_cp_track is None:
return 0 return 0
elif current_track.length is None: elif current_cp_track.track.length is None:
return 0 return 0
else: else:
return current_track.length return current_cp_track.track.length
def _status_volume(frontend): def _status_volume(futures):
volume = frontend.mixer.volume.get() volume = futures['mixer.volume'].get()
if volume is not None: if volume is not None:
return volume return volume
else: else:
return 0 return 0
def _status_xfade(frontend): def _status_xfade(futures):
return 0 # TODO return 0 # Not supported

View File

@ -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 from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" ' @handle_request(r'^sticker delete "(?P<field>[^"]+)" '
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$') r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
def sticker_delete(frontend, field, uri, name=None): def sticker_delete(context, field, uri, name=None):
""" """
*musicpd.org, sticker section:* *musicpd.org, sticker section:*
@ -14,9 +14,9 @@ def sticker_delete(frontend, field, uri, name=None):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" ' @handle_request(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)"$') r'"(?P<name>[^"]+)"$')
def sticker_find(frontend, field, uri, name): def sticker_find(context, field, uri, name):
""" """
*musicpd.org, sticker section:* *musicpd.org, sticker section:*
@ -28,9 +28,9 @@ def sticker_find(frontend, field, uri, name):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" ' @handle_request(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)"$') r'"(?P<name>[^"]+)"$')
def sticker_get(frontend, field, uri, name): def sticker_get(context, field, uri, name):
""" """
*musicpd.org, sticker section:* *musicpd.org, sticker section:*
@ -40,8 +40,8 @@ def sticker_get(frontend, field, uri, name):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$') @handle_request(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
def sticker_list(frontend, field, uri): def sticker_list(context, field, uri):
""" """
*musicpd.org, sticker section:* *musicpd.org, sticker section:*
@ -51,9 +51,9 @@ def sticker_list(frontend, field, uri):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" ' @handle_request(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$') r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
def sticker_set(frontend, field, uri, name, value): def sticker_set(context, field, uri, name, value):
""" """
*musicpd.org, sticker section:* *musicpd.org, sticker section:*

View File

@ -1,10 +1,10 @@
import datetime as dt 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 from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$') @handle_request(r'^listplaylist "(?P<name>[^"]+)"$')
def listplaylist(frontend, name): def listplaylist(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -19,13 +19,13 @@ def listplaylist(frontend, name):
file: relative/path/to/file3.mp3 file: relative/path/to/file3.mp3
""" """
try: 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] return ['file: %s' % t.uri for t in playlist.tracks]
except LookupError: except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'listplaylist') raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$') @handle_request(r'^listplaylistinfo "(?P<name>[^"]+)"$')
def listplaylistinfo(frontend, name): def listplaylistinfo(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -39,14 +39,14 @@ def listplaylistinfo(frontend, name):
Album, Artist, Track Album, Artist, Track
""" """
try: try:
playlist = frontend.backend.stored_playlists.get(name=name).get() playlist = context.backend.stored_playlists.get(name=name).get()
return playlist.mpd_format() return playlist.mpd_format()
except LookupError: except LookupError:
raise MpdNoExistError( raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo') u'No such playlist', command=u'listplaylistinfo')
@handle_pattern(r'^listplaylists$') @handle_request(r'^listplaylists$')
def listplaylists(frontend): def listplaylists(context):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -67,7 +67,7 @@ def listplaylists(frontend):
Last-Modified: 2010-02-06T02:11:08Z Last-Modified: 2010-02-06T02:11:08Z
""" """
result = [] 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)) result.append((u'playlist', playlist.name))
last_modified = (playlist.last_modified or last_modified = (playlist.last_modified or
dt.datetime.now()).isoformat() dt.datetime.now()).isoformat()
@ -79,8 +79,8 @@ def listplaylists(frontend):
result.append((u'Last-Modified', last_modified)) result.append((u'Last-Modified', last_modified))
return result return result
@handle_pattern(r'^load "(?P<name>[^"]+)"$') @handle_request(r'^load "(?P<name>[^"]+)"$')
def load(frontend, name): def load(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -93,13 +93,13 @@ def load(frontend, name):
- ``load`` appends the given playlist to the current playlist. - ``load`` appends the given playlist to the current playlist.
""" """
try: try:
playlist = frontend.backend.stored_playlists.get(name=name).get() playlist = context.backend.stored_playlists.get(name=name).get()
frontend.backend.current_playlist.append(playlist.tracks) context.backend.current_playlist.append(playlist.tracks)
except LookupError: except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'load') raise MpdNoExistError(u'No such playlist', command=u'load')
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$') @handle_request(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
def playlistadd(frontend, name, uri): def playlistadd(context, name, uri):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -111,8 +111,8 @@ def playlistadd(frontend, name, uri):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^playlistclear "(?P<name>[^"]+)"$') @handle_request(r'^playlistclear "(?P<name>[^"]+)"$')
def playlistclear(frontend, name): def playlistclear(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -122,8 +122,8 @@ def playlistclear(frontend, name):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^playlistdelete "(?P<name>[^"]+)" "(?P<songpos>\d+)"$') @handle_request(r'^playlistdelete "(?P<name>[^"]+)" "(?P<songpos>\d+)"$')
def playlistdelete(frontend, name, songpos): def playlistdelete(context, name, songpos):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -133,9 +133,9 @@ def playlistdelete(frontend, name, songpos):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^playlistmove "(?P<name>[^"]+)" ' @handle_request(r'^playlistmove "(?P<name>[^"]+)" '
r'"(?P<from_pos>\d+)" "(?P<to_pos>\d+)"$') r'"(?P<from_pos>\d+)" "(?P<to_pos>\d+)"$')
def playlistmove(frontend, name, from_pos, to_pos): def playlistmove(context, name, from_pos, to_pos):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -152,8 +152,8 @@ def playlistmove(frontend, name, from_pos, to_pos):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$') @handle_request(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
def rename(frontend, old_name, new_name): def rename(context, old_name, new_name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -163,8 +163,8 @@ def rename(frontend, old_name, new_name):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^rm "(?P<name>[^"]+)"$') @handle_request(r'^rm "(?P<name>[^"]+)"$')
def rm(frontend, name): def rm(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*
@ -174,8 +174,8 @@ def rm(frontend, name):
""" """
raise MpdNotImplemented # TODO raise MpdNotImplemented # TODO
@handle_pattern(r'^save "(?P<name>[^"]+)"$') @handle_request(r'^save "(?P<name>[^"]+)"$')
def save(frontend, name): def save(context, name):
""" """
*musicpd.org, stored playlists section:* *musicpd.org, stored playlists section:*

View File

@ -52,18 +52,19 @@ class MpdServer(asyncore.dispatcher):
self._format_hostname(settings.MPD_SERVER_HOSTNAME), self._format_hostname(settings.MPD_SERVER_HOSTNAME),
settings.MPD_SERVER_PORT) settings.MPD_SERVER_PORT)
except IOError, e: 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) sys.exit(1)
def handle_accept(self): def handle_accept(self):
"""Handle new client connection.""" """Called by asyncore when a new client connects."""
(client_socket, client_socket_address) = self.accept() (client_socket, client_socket_address) = self.accept()
logger.info(u'MPD client connection from [%s]:%s', logger.info(u'MPD client connection from [%s]:%s',
client_socket_address[0], client_socket_address[1]) 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): def handle_close(self):
"""Handle end of client connection.""" """Called by asyncore when the socket is closed."""
self.close() self.close()
def _format_hostname(self, hostname): def _format_hostname(self, hostname):

View File

@ -1,7 +1,6 @@
import asynchat import asynchat
import logging import logging
from mopidy import settings
from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent from mopidy.utils.log import indent
@ -22,70 +21,38 @@ class MpdSession(asynchat.async_chat):
self.input_buffer = [] self.input_buffer = []
self.authenticated = False self.authenticated = False
self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
self.dispatcher = MpdDispatcher() self.dispatcher = MpdDispatcher(session=self)
self.send_response([u'OK MPD %s' % VERSION])
def start(self):
"""Start a new client session."""
self.send_response(u'OK MPD %s' % VERSION)
def collect_incoming_data(self, data): 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) self.input_buffer.append(data)
def found_terminator(self): 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() data = ''.join(self.input_buffer).strip()
self.input_buffer = [] self.input_buffer = []
try: try:
request = data.decode(ENCODING) self.send_response(self.handle_request(data))
logger.debug(u'Input from [%s]:%s: %s', self.client_address,
self.client_port, indent(request))
self.handle_request(request)
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
logger.warning(u'Received invalid data: %s', e) logger.warning(u'Received invalid data: %s', e)
def handle_request(self, request): def handle_request(self, request):
"""Handle request by sending it to the MPD frontend.""" """Handle the request using the MPD command handlers."""
if not self.authenticated: request = request.decode(ENCODING)
(self.authenticated, response) = self.check_password(request) logger.debug(u'Request from [%s]:%s: %s', self.client_address,
if response is not None: self.client_port, indent(request))
self.send_response(response) return self.dispatcher.handle_request(request)
return
response = 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: if response is not None:
self.handle_response(response) response = LINE_TERMINATOR.join(response)
logger.debug(u'Response to [%s]:%s: %s', self.client_address,
def handle_response(self, response): self.client_port, indent(response))
"""Handle response from the MPD frontend.""" response = u'%s%s' % (response, LINE_TERMINATOR)
self.send_response(LINE_TERMINATOR.join(response)) data = response.encode(ENCODING)
self.push(data)
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})

View File

@ -36,71 +36,71 @@ class GStreamer(ThreadingActor):
def __init__(self): def __init__(self):
self._pipeline = None self._pipeline = None
self._source = None self._source = None
self._taginject = None
self._tee = None self._tee = None
self._uridecodebin = None self._uridecodebin = None
self._volume = None self._volume = None
self._outputs = []
self._handlers = {}
def on_start(self): 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): def _setup_pipeline(self):
"""
**Warning:** :class:`GStreamer` requires
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
not enforced by :class:`GStreamer` itself.
"""
description = ' ! '.join([ description = ' ! '.join([
'uridecodebin name=uri', 'uridecodebin name=uri',
'audioconvert name=convert', 'audioconvert name=convert',
'volume name=volume', 'volume name=volume',
'taginject name=inject',
'tee name=tee']) 'tee name=tee'])
logger.debug(u'Setting up base GStreamer pipeline: %s', description) logger.debug(u'Setting up base GStreamer pipeline: %s', description)
self._pipeline = gst.parse_launch(description) self._pipeline = gst.parse_launch(description)
self._taginject = self._pipeline.get_by_name('inject')
self._tee = self._pipeline.get_by_name('tee') self._tee = self._pipeline.get_by_name('tee')
self._volume = self._pipeline.get_by_name('volume') self._volume = self._pipeline.get_by_name('volume')
self._uridecodebin = self._pipeline.get_by_name('uri') self._uridecodebin = self._pipeline.get_by_name('uri')
self._uridecodebin.connect('notify::source', self._process_new_source) self._uridecodebin.connect('notify::source', self._on_new_source)
self._uridecodebin.connect('pad-added', self._process_new_pad, self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('convert').get_pad('sink')) self._pipeline.get_by_name('convert').get_pad('sink'))
def _setup_outputs(self):
for output in settings.OUTPUTS: for output in settings.OUTPUTS:
output_cls = get_class(output)() get_class(output)(self).connect()
output_cls.connect_bin(self._pipeline, self._tee)
# Setup bus and message processor def _setup_message_processor(self):
bus = self._pipeline.get_bus() bus = self._pipeline.get_bus()
bus.add_signal_watch() bus.add_signal_watch()
bus.connect('message', self._process_gstreamer_message) bus.connect('message', self._on_message)
def _process_new_source(self, element, pad): def _on_new_source(self, element, pad):
self._source = element.get_by_name('source') self._source = element.get_property('source')
try: try:
self._source.set_property('caps', default_caps) self._source.set_property('caps', default_caps)
except TypeError: except TypeError:
pass pass
def _process_new_pad(self, source, pad, target_pad): def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked(): if not pad.is_linked():
pad.link(target_pad) pad.link(target_pad)
def _process_gstreamer_message(self, bus, message): def _on_message(self, bus, message):
"""Process messages from GStreamer.""" if message.src in self._handlers:
if self._handlers[message.src](message):
return # Message was handeled by output
if message.type == gst.MESSAGE_EOS: if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. ' logger.debug(u'GStreamer signalled end-of-stream. '
'Telling backend ...') 'Telling backend ...')
self._get_backend().playback.on_end_of_track() self._get_backend().playback.on_end_of_track()
elif message.type == gst.MESSAGE_ERROR: elif message.type == gst.MESSAGE_ERROR:
self.stop_playback()
error, debug = message.parse_error() error, debug = message.parse_error()
logger.error(u'%s %s', error, debug) logger.error(u'%s %s', error, debug)
# FIXME Should we send 'stop_playback' to the backend here? Can we self.stop_playback()
# differentiate on how serious the error is?
elif message.type == gst.MESSAGE_WARNING: elif message.type == gst.MESSAGE_WARNING:
error, debug = message.parse_warning() error, debug = message.parse_warning()
logger.warning(u'%s %s', error, debug) logger.warning(u'%s %s', error, debug)
@ -112,16 +112,18 @@ class GStreamer(ThreadingActor):
def set_uri(self, uri): 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 :param uri: the URI to play
:type uri: string :type uri: string
""" """
self._uridecodebin.set_property('uri', uri) 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 :param capabilities: a GStreamer capabilities string
:type capabilities: string :type capabilities: string
@ -133,9 +135,10 @@ class GStreamer(ThreadingActor):
self._source.set_property('caps', caps) self._source.set_property('caps', caps)
self._source.emit('push-buffer', buffer_) 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 We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks. token, and can then do any end-of-stream related tasks.
@ -172,18 +175,26 @@ class GStreamer(ThreadingActor):
return handeled return handeled
def start_playback(self): 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) return self._set_state(gst.STATE_PLAYING)
def pause_playback(self): 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) return self._set_state(gst.STATE_PAUSED)
def prepare_change(self): def prepare_change(self):
""" """
Notify GStreamer that we are about to change state of playback. 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 changes like updating data that is being pushed. The reason for this
is that GStreamer will reset all its state when it changes to is that GStreamer will reset all its state when it changes to
:attr:`gst.STATE_READY`. :attr:`gst.STATE_READY`.
@ -191,15 +202,22 @@ class GStreamer(ThreadingActor):
return self._set_state(gst.STATE_READY) return self._set_state(gst.STATE_READY)
def stop_playback(self): 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) return self._set_state(gst.STATE_NULL)
def _set_state(self, state): 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 .. digraph:: gst_state_transitions
graph [rankdir="LR"];
node [fontsize=10];
"NULL" -> "READY" "NULL" -> "READY"
"PAUSED" -> "PLAYING" "PAUSED" -> "PLAYING"
"PAUSED" -> "READY" "PAUSED" -> "READY"
@ -210,7 +228,7 @@ class GStreamer(ThreadingActor):
:param state: State to set pipeline to. One of: `gst.STATE_NULL`, :param state: State to set pipeline to. One of: `gst.STATE_NULL`,
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
:type state: :class:`gst.State` :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) result = self._pipeline.set_state(state)
if result == gst.STATE_CHANGE_FAILURE: if result == gst.STATE_CHANGE_FAILURE:
@ -228,7 +246,7 @@ class GStreamer(ThreadingActor):
def get_volume(self): def get_volume(self):
""" """
Get volume level for software mixer. Get volume level of the GStreamer software mixer.
:rtype: int in range [0..100] :rtype: int in range [0..100]
""" """
@ -236,7 +254,7 @@ class GStreamer(ThreadingActor):
def set_volume(self, volume): 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] :param volume: the volume in the range [0..100]
:type volume: int :type volume: int
@ -249,16 +267,122 @@ class GStreamer(ThreadingActor):
""" """
Set track metadata for currently playing song. Set track metadata for currently playing song.
Only needs to be called by sources such as appsrc which don't already Only needs to be called by sources such as `appsrc` which do not
inject tags in pipeline. 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` :type track: :class:`mopidy.modes.Track`
""" """
# FIXME what if we want to unset taginject tags? taglist = gst.TagList()
tags = u'artist="%(artist)s",title="%(title)s"' % { artists = [a for a in (track.artists or []) if a.name]
'artist': u', '.join([a.name for a in track.artists]),
'title': track.name, if artists:
} taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
logger.debug('Setting tags to: %s', tags) if track.name:
self._taginject.set_property('tags', tags) 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)

View File

@ -51,9 +51,9 @@ class AlsaMixer(ThreadingActor, BaseMixer):
return [settings.MIXER_ALSA_CONTROL] return [settings.MIXER_ALSA_CONTROL]
return [u'Master', u'PCM'] return [u'Master', u'PCM']
def _get_volume(self): def get_volume(self):
# FIXME does not seem to see external volume changes. # FIXME does not seem to see external volume changes.
return self._mixer.getvolume()[0] return self._mixer.getvolume()[0]
def _set_volume(self, volume): def set_volume(self, volume):
self._mixer.setvolume(volume) self._mixer.setvolume(volume)

View File

@ -17,9 +17,10 @@ class BaseMixer(object):
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
equal to 0. Values above 100 is equal to 100. 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 None
return int(self._get_volume() / self.amplification_factor) return int(volume / self.amplification_factor)
@volume.setter @volume.setter
def volume(self, volume): def volume(self, volume):
@ -28,9 +29,9 @@ class BaseMixer(object):
volume = 0 volume = 0
elif volume > 100: elif volume > 100:
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. Return volume as integer in range [0, 100]. :class:`None` if unknown.
@ -38,7 +39,7 @@ class BaseMixer(object):
""" """
raise NotImplementedError raise NotImplementedError
def _set_volume(self, volume): def set_volume(self, volume):
""" """
Set volume as integer in range [0, 100]. Set volume as integer in range [0, 100].

View File

@ -35,14 +35,14 @@ class DenonMixer(ThreadingActor, BaseMixer):
from serial import Serial from serial import Serial
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
def _get_volume(self): def get_volume(self):
self._ensure_open_device() self._ensure_open_device()
self._device.write('MV?\r') self._device.write('MV?\r')
vol = str(self._device.readline()[2:4]) vol = str(self._device.readline()[2:4])
logger.debug(u'_get_volume() = %s' % vol) logger.debug(u'_get_volume() = %s' % vol)
return self._levels.index(vol) return self._levels.index(vol)
def _set_volume(self, volume): def set_volume(self, volume):
# Clamp according to Denon-spec # Clamp according to Denon-spec
if volume > 99: if volume > 99:
volume = 99 volume = 99

View File

@ -8,8 +8,8 @@ class DummyMixer(ThreadingActor, BaseMixer):
def __init__(self): def __init__(self):
self._volume = None self._volume = None
def _get_volume(self): def get_volume(self):
return self._volume return self._volume
def _set_volume(self, volume): def set_volume(self, volume):
self._volume = volume self._volume = volume

View File

@ -15,8 +15,8 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
assert len(output_refs) == 1, 'Expected exactly one running output.' assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy() self.output = output_refs[0].proxy()
def _get_volume(self): def get_volume(self):
return self.output.get_volume().get() return self.output.get_volume().get()
def _set_volume(self, volume): def set_volume(self, volume):
self.output.set_volume(volume).get() self.output.set_volume(volume).get()

View File

@ -40,10 +40,10 @@ class NadMixer(ThreadingActor, BaseMixer):
self._volume_cache = None self._volume_cache = None
self._nad_talker = NadTalker.start().proxy() self._nad_talker = NadTalker.start().proxy()
def _get_volume(self): def get_volume(self):
return self._volume_cache return self._volume_cache
def _set_volume(self, volume): def set_volume(self, volume):
self._volume_cache = volume self._volume_cache = volume
self._nad_talker.set_volume(volume) self._nad_talker.set_volume(volume)

View File

@ -28,7 +28,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
and self._last_update is not None and self._last_update is not None
and (int(time.time() - self._last_update) < self.CACHE_TTL)) and (int(time.time() - self._last_update) < self.CACHE_TTL))
def _get_volume(self): def get_volume(self):
if not self._valid_cache(): if not self._valid_cache():
try: try:
self._cache = int(Popen( self._cache = int(Popen(
@ -40,7 +40,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
self._last_update = int(time.time()) self._last_update = int(time.time())
return self._cache return self._cache
def _set_volume(self, volume): def set_volume(self, volume):
Popen(['osascript', '-e', 'set volume output volume %d' % volume]) Popen(['osascript', '-e', 'set volume output volume %d' % volume])
self._cache = volume self._cache = volume
self._last_update = int(time.time()) self._last_update = int(time.time())

View File

@ -1,4 +1,4 @@
from mopidy.frontends.mpd import translator from collections import namedtuple
class ImmutableObject(object): class ImmutableObject(object):
""" """
@ -129,6 +129,9 @@ class Album(ImmutableObject):
super(Album, self).__init__(*args, **kwargs) super(Album, self).__init__(*args, **kwargs)
CpTrack = namedtuple('CpTrack', ['cpid', 'track'])
class Track(ImmutableObject): class Track(ImmutableObject):
""" """
:param uri: track URI :param uri: track URI
@ -183,6 +186,7 @@ class Track(ImmutableObject):
super(Track, self).__init__(*args, **kwargs) super(Track, self).__init__(*args, **kwargs)
def mpd_format(self, *args, **kwargs): def mpd_format(self, *args, **kwargs):
from mopidy.frontends.mpd import translator
return translator.track_to_mpd_format(self, *args, **kwargs) return translator.track_to_mpd_format(self, *args, **kwargs)
@ -222,4 +226,5 @@ class Playlist(ImmutableObject):
return len(self.tracks) return len(self.tracks)
def mpd_format(self, *args, **kwargs): def mpd_format(self, *args, **kwargs):
from mopidy.frontends.mpd import translator
return translator.playlist_to_mpd_format(self, *args, **kwargs) return translator.playlist_to_mpd_format(self, *args, **kwargs)

View File

@ -1,153 +1,105 @@
import logging
import pygst import pygst
pygst.require('0.10') pygst.require('0.10')
import gst import gst
from mopidy import settings import logging
logger = logging.getLogger('mopidy.outputs') logger = logging.getLogger('mopidy.outputs')
class BaseOutput(object): class BaseOutput(object):
"""Base class for providing support for multiple pluggable outputs.""" """Base class for pluggable audio outputs."""
def connect_bin(self, pipeline, element): MESSAGE_EOS = gst.MESSAGE_EOS
""" MESSAGE_ERROR = gst.MESSAGE_ERROR
Connect output bin to pipeline and given element. MESSAGE_WARNING = gst.MESSAGE_WARNING
In normal cases the element will probably be a `tee`, def __init__(self, gstreamer):
thus allowing us to connect any number of outputs. This self.gstreamer = gstreamer
however is why each bin is forced to have its own `queue` self.bin = self._build_bin()
after the `tee`. self.bin.set_name(self.get_name())
:param pipeline: gst.Pipeline to add output to. self.modify_bin()
:type pipeline: :class:`gst.Pipeline`
:param element: gst.Element in pipeline to connect output to. def _build_bin(self):
:type element: :class:`gst.Element`
"""
description = 'queue ! %s' % self.describe_bin() 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) def connect(self):
self.modify_bin(output) """Attach output to GStreamer pipeline."""
self.gstreamer.connect_output(self.bin)
self.on_connect()
pipeline.add(output) def on_connect(self):
output.sync_state_with_parent() # Required to add to running pipe
gst.element_link_many(element, output)
def modify_bin(self, output):
""" """
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 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 before it is installed. This can for instance be a good place to call
`set_properties` on elements that need to be configured. `set_properties` on elements that need to be configured.
:param output: gst.Bin to modify in some way. *MAY be implemented by subclass.*
:type output: :class:`gst.Bin`
""" """
pass pass
def describe_bin(self): 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` For simple cases this can just be a sink such as ``autoaudiosink``,
or it can be a chain `element1 ! element2 ! sink`. See `man or it can be a chain like ``element1 ! element2 ! sink``. See the
gst-launch0.10` for details on format. manpage of :command:`gst-launch` for details on the format.
*MUST be implemented by subclass.* *MUST be implemented by subclass.*
:rtype: string
""" """
raise NotImplementedError raise NotImplementedError
def set_properties(self, element, properties): 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 Will call :meth:`gst.Element.set_property` on ``element`` for each key
that is not None. 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` :type element: :class:`gst.Element`
:param properties: Dictionary of properties to set on element. :param properties: properties to set on element
:type properties: dict :type properties: dict
""" """
for key, value in properties.items(): for key, value in properties.items():
if value is not None: if value is not None:
element.set_property(key, value) 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,
})

26
mopidy/outputs/custom.py Normal file
View File

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

12
mopidy/outputs/local.py Normal file
View File

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

View File

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

View File

@ -174,10 +174,10 @@ MPD_SERVER_PORT = 6600
#: Default:: #: Default::
#: #:
#: OUTPUTS = ( #: OUTPUTS = (
#: u'mopidy.outputs.LocalOutput', #: u'mopidy.outputs.local.LocalOutput',
#: ) #: )
OUTPUTS = ( OUTPUTS = (
u'mopidy.outputs.LocalOutput', u'mopidy.outputs.local.LocalOutput',
) )
#: Servar that runs Shoutcast server to send stream to. #: Servar that runs Shoutcast server to send stream to.

View File

@ -4,6 +4,8 @@ import threading
import gobject import gobject
gobject.threads_init() gobject.threads_init()
from pykka import ActorDeadError
from mopidy import SettingsError from mopidy import SettingsError
logger = logging.getLogger('mopidy.utils.process') logger = logging.getLogger('mopidy.utils.process')
@ -21,26 +23,19 @@ class BaseThread(threading.Thread):
self.run_inside_try() self.run_inside_try()
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info(u'Interrupted by user') logger.info(u'Interrupted by user')
self.exit(0, u'Interrupted by user')
except SettingsError as e: except SettingsError as e:
logger.error(e.message) logger.error(e.message)
self.exit(1, u'Settings error')
except ImportError as e: except ImportError as e:
logger.error(e) logger.error(e)
self.exit(2, u'Import error') except ActorDeadError as e:
logger.warning(e)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
self.exit(3, u'Unknown error') logger.debug(u'%s: Exiting thread', self.name)
def run_inside_try(self): def run_inside_try(self):
raise NotImplementedError raise NotImplementedError
def destroy(self):
pass
def exit(self, status=0, reason=None):
self.destroy()
class GObjectEventThread(BaseThread): class GObjectEventThread(BaseThread):
""" """

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import
from copy import copy from copy import copy
import logging import logging
import os import os
from pprint import pformat
import sys import sys
from mopidy import SettingsError from mopidy import SettingsError
@ -140,19 +141,22 @@ def list_settings_optparse_callback(*args):
option. option.
""" """
from mopidy import settings from mopidy import settings
print format_settings_list(settings)
sys.exit(0)
def format_settings_list(settings):
errors = settings.get_errors() errors = settings.get_errors()
lines = [] lines = []
for (key, value) in sorted(settings.current.iteritems()): for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key) default_value = settings.default.get(key)
value = mask_value_if_secret(key, value) masked_value = mask_value_if_secret(key, value)
lines.append(u'%s:' % key) lines.append(u'%s: %s' % (key, indent(pformat(masked_value), places=2)))
lines.append(u' Value: %s' % repr(value))
if value != default_value and default_value is not None: 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: if errors.get(key) is not None:
lines.append(u' Error: %s' % errors[key]) lines.append(u' Error: %s' % errors[key])
print u'Settings: %s' % indent('\n'.join(lines), places=2) return '\n'.join(lines)
sys.exit(0)
def mask_value_if_secret(key, value): def mask_value_if_secret(key, value):
if key.endswith('PASSWORD') and value: if key.endswith('PASSWORD') and value:

View File

@ -23,14 +23,14 @@ class CurrentPlaylistControllerTest(object):
cp_track = self.controller.add(track) cp_track = self.controller.add(track)
self.assertEqual(track, self.controller.tracks[-1]) self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(cp_track, self.controller.cp_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): def test_add_at_position(self):
for track in self.tracks[:-1]: for track in self.tracks[:-1]:
cp_track = self.controller.add(track, 0) cp_track = self.controller.add(track, 0)
self.assertEqual(track, self.controller.tracks[0]) self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(cp_track, self.controller.cp_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 @populate_playlist
def test_add_at_position_outside_of_playlist(self): def test_add_at_position_outside_of_playlist(self):
@ -40,12 +40,12 @@ class CurrentPlaylistControllerTest(object):
@populate_playlist @populate_playlist
def test_get_by_cpid(self): def test_get_by_cpid(self):
cp_track = self.controller.cp_tracks[1] 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 @populate_playlist
def test_get_by_uri(self): def test_get_by_uri(self):
cp_track = self.controller.cp_tracks[1] 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 @populate_playlist
def test_get_by_uri_raises_error_for_invalid_uri(self): def test_get_by_uri_raises_error_for_invalid_uri(self):

View File

@ -1,29 +1,29 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase): class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_enableoutput(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_disableoutput(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_outputs(self): 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'outputid: 0' in result)
self.assert_(u'outputname: None' in result) self.assert_(u'outputname: None' in result)
self.assert_(u'outputenabled: 1' in result) self.assert_(u'outputenabled: 1' in result)

View File

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

View File

@ -8,55 +8,56 @@ class CommandListsTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.b = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = dispatcher.MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.b.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_command_list_begin(self): def test_command_list_begin(self):
result = self.h.handle_request(u'command_list_begin') result = self.dispatcher.handle_request(u'command_list_begin')
self.assert_(result is None) self.assertEquals(result, [])
def test_command_list_end(self): def test_command_list_end(self):
self.h.handle_request(u'command_list_begin') self.dispatcher.handle_request(u'command_list_begin')
result = self.h.handle_request(u'command_list_end') result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_command_list_end_without_start_first_is_an_unknown_command(self): 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], self.assertEquals(result[0],
u'ACK [5@0] {} unknown command "command_list_end"') u'ACK [5@0] {} unknown command "command_list_end"')
def test_command_list_with_ping(self): def test_command_list_with_ping(self):
self.h.handle_request(u'command_list_begin') self.dispatcher.handle_request(u'command_list_begin')
self.assertEqual([], self.h.command_list) self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(False, self.h.command_list_ok) self.assertEqual(False, self.dispatcher.command_list_ok)
self.h.handle_request(u'ping') self.dispatcher.handle_request(u'ping')
self.assert_(u'ping' in self.h.command_list) self.assert_(u'ping' in self.dispatcher.command_list)
result = self.h.handle_request(u'command_list_end') result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result) 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): def test_command_list_with_error_returns_ack_with_correct_index(self):
self.h.handle_request(u'command_list_begin') self.dispatcher.handle_request(u'command_list_begin')
self.h.handle_request(u'play') # Known command self.dispatcher.handle_request(u'play') # Known command
self.h.handle_request(u'paly') # Unknown command self.dispatcher.handle_request(u'paly') # Unknown command
result = self.h.handle_request(u'command_list_end') 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"') self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self): def test_command_list_ok_begin(self):
result = self.h.handle_request(u'command_list_ok_begin') result = self.dispatcher.handle_request(u'command_list_ok_begin')
self.assert_(result is None) self.assertEquals(result, [])
def test_command_list_ok_with_ping(self): def test_command_list_ok_with_ping(self):
self.h.handle_request(u'command_list_ok_begin') self.dispatcher.handle_request(u'command_list_ok_begin')
self.assertEqual([], self.h.command_list) self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(True, self.h.command_list_ok) self.assertEqual(True, self.dispatcher.command_list_ok)
self.h.handle_request(u'ping') self.dispatcher.handle_request(u'ping')
self.assert_(u'ping' in self.h.command_list) self.assert_(u'ping' in self.dispatcher.command_list)
result = self.h.handle_request(u'command_list_end') result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'list_OK' in result) self.assert_(u'list_OK' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(False, self.h.command_list) self.assertEqual(False, self.dispatcher.command_list)
self.assertEqual(False, self.h.command_list_ok) self.assertEqual(False, self.dispatcher.command_list_ok)

View File

@ -1,48 +1,53 @@
import mock
import unittest import unittest
from mopidy import settings from mopidy import settings
from mopidy.backends.dummy import DummyBackend 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 from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase): class ConnectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.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): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
settings.runtime.clear() settings.runtime.clear()
def test_close(self): def test_close_closes_the_client_connection(self):
result = self.h.handle_request(u'close') 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) self.assert_(u'OK' in result)
def test_empty_request(self): def test_empty_request(self):
result = self.h.handle_request(u'') result = self.dispatcher.handle_request(u'')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_kill(self): def test_kill(self):
result = self.h.handle_request(u'kill') result = self.dispatcher.handle_request(u'kill')
self.assert_(u'OK' in result) self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
def test_valid_password_is_accepted(self): def test_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret' 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) self.assert_(u'OK' in result)
def test_invalid_password_is_not_accepted(self): def test_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret' 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) self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_any_password_is_not_accepted_when_password_check_turned_off(self): def test_any_password_is_not_accepted_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None 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) self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_ping(self): def test_ping(self):
result = self.h.handle_request(u'ping') result = self.dispatcher.handle_request(u'ping')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)

View File

@ -1,160 +1,160 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase): class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_add(self): def test_add(self):
needle = Track(uri='dummy://foo') needle = Track(uri='dummy://foo')
self.b.library.provider.dummy_library = [ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()] Track(), Track(), needle, Track()]
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'add "dummy://foo"') result = self.dispatcher.handle_request(u'add "dummy://foo"')
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual(result[0], u'OK') self.assertEqual(result[0], u'OK')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
def test_add_with_uri_not_found_in_library_should_ack(self): 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], self.assertEqual(result[0],
u'ACK [50@0] {add} directory or file not found') u'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self): 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) # TODO check that we add all tracks (we currently don't)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_addid_without_songpos(self): def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo') needle = Track(uri='dummy://foo')
self.b.library.provider.dummy_library = [ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()] Track(), Track(), needle, Track()]
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo"') result = self.dispatcher.handle_request(u'addid "dummy://foo"')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0] self.assert_(u'Id: %d' %
in result) self.backend.current_playlist.cp_tracks.get()[5][0] in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_addid_with_empty_uri_acks(self): 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') self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self): def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo') needle = Track(uri='dummy://foo')
self.b.library.provider.dummy_library = [ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()] Track(), Track(), needle, Track()]
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo" "3"') result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.b.current_playlist.tracks.get()[3], needle) self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0] self.assert_(u'Id: %d' %
in result) self.backend.current_playlist.cp_tracks.get()[3][0] in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_addid_with_songpos_out_of_bounds_should_ack(self): def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo') needle = Track(uri='dummy://foo')
self.b.library.provider.dummy_library = [ self.backend.library.provider.dummy_library = [
Track(), Track(), needle, Track()] Track(), Track(), needle, Track()]
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo" "6"') result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index') self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self): 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') self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_clear(self): def test_clear(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'clear') result = self.dispatcher.handle_request(u'clear')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 0) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_delete_songpos(self): def test_delete_songpos(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "%d"' % result = self.dispatcher.handle_request(u'delete "%d"' %
self.b.current_playlist.cp_tracks.get()[2][0]) self.backend.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()), 4)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_delete_songpos_out_of_bounds(self): def test_delete_songpos_out_of_bounds(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "5"') result = self.dispatcher.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)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self): def test_delete_open_range(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "1:"') result = self.dispatcher.handle_request(u'delete "1:"')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_delete_closed_range(self): def test_delete_closed_range(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "1:3"') result = self.dispatcher.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()), 3)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_delete_range_out_of_bounds(self): def test_delete_range_out_of_bounds(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()]) [Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "5:7"') result = self.dispatcher.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)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self): def test_deleteid(self):
self.b.current_playlist.append([Track(), Track()]) self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
result = self.h.handle_request(u'deleteid "1"') result = self.dispatcher.handle_request(u'deleteid "1"')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_deleteid_does_not_exist(self): def test_deleteid_does_not_exist(self):
self.b.current_playlist.append([Track(), Track()]) self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
result = self.h.handle_request(u'deleteid "12345"') result = self.dispatcher.handle_request(u'deleteid "12345"')
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song') self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'move "1" "0"') result = self.dispatcher.handle_request(u'move "1" "0"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'a') self.assertEqual(tracks[1].name, 'a')
self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[2].name, 'c')
@ -164,12 +164,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_move_open_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'move "2:" "0"') result = self.dispatcher.handle_request(u'move "2:" "0"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'c') self.assertEqual(tracks[0].name, 'c')
self.assertEqual(tracks[1].name, 'd') self.assertEqual(tracks[1].name, 'd')
self.assertEqual(tracks[2].name, 'e') self.assertEqual(tracks[2].name, 'e')
@ -179,12 +179,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_move_closed_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'move "1:3" "0"') result = self.dispatcher.handle_request(u'move "1:3" "0"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'c') self.assertEqual(tracks[1].name, 'c')
self.assertEqual(tracks[2].name, 'a') self.assertEqual(tracks[2].name, 'a')
@ -194,12 +194,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_moveid(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'moveid "4" "2"') result = self.dispatcher.handle_request(u'moveid "4" "2"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b') self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'e') self.assertEqual(tracks[2].name, 'e')
@ -209,30 +209,30 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlist_returns_same_as_playlistinfo(self): def test_playlist_returns_same_as_playlistinfo(self):
playlist_result = self.h.handle_request(u'playlist') playlist_result = self.dispatcher.handle_request(u'playlist')
playlistinfo_result = self.h.handle_request(u'playlistinfo') playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(playlist_result, playlistinfo_result) self.assertEqual(playlist_result, playlistinfo_result)
def test_playlistfind(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistfind_by_filename_not_in_current_playlist(self): 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"') u'playlistfind "filename" "file:///dev/null"')
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistfind_by_filename_without_quotes(self): def test_playlistfind_by_filename_without_quotes(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'playlistfind filename "file:///dev/null"') u'playlistfind filename "file:///dev/null"')
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistfind_by_filename_in_current_playlist(self): def test_playlistfind_by_filename_in_current_playlist(self):
self.b.current_playlist.append([ self.backend.current_playlist.append([
Track(uri='file:///exists')]) Track(uri='file:///exists')])
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'playlistfind filename "file:///exists"') u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result) self.assert_(u'file: file:///exists' in result)
self.assert_(u'Id: 0' in result) self.assert_(u'Id: 0' in result)
@ -240,15 +240,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistid_without_songid(self): def test_playlistid_without_songid(self):
self.b.current_playlist.append([Track(name='a'), Track(name='b')]) self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid') result = self.dispatcher.handle_request(u'playlistid')
self.assert_(u'Title: a' in result) self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistid_with_songid(self): def test_playlistid_with_songid(self):
self.b.current_playlist.append([Track(name='a'), Track(name='b')]) self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid "1"') result = self.dispatcher.handle_request(u'playlistid "1"')
self.assert_(u'Title: a' not in result) self.assert_(u'Title: a' not in result)
self.assert_(u'Id: 0' not in result) self.assert_(u'Id: 0' not in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
@ -256,16 +256,16 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistid_with_not_existing_songid_fails(self): def test_playlistid_with_not_existing_songid_fails(self):
self.b.current_playlist.append([Track(name='a'), Track(name='b')]) self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid "25"') result = self.dispatcher.handle_request(u'playlistid "25"')
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song') self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), 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: a' in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
@ -275,11 +275,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistinfo_with_songpos(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), 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: a' not in result)
self.assert_(u'Title: b' not in result) self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' 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) self.assert_(u'OK' in result)
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self): def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
result1 = self.h.handle_request(u'playlistinfo "-1"') result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
result2 = self.h.handle_request(u'playlistinfo') result2 = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(result1, result2) self.assertEqual(result1, result2)
def test_playlistinfo_with_open_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), 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: a' not in result)
self.assert_(u'Title: b' not in result) self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
@ -308,11 +308,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistinfo_with_closed_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), 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: a' not in result)
self.assert_(u'Title: b' not in result) self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
@ -322,52 +322,53 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self): 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) self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self): 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) self.assert_(u'OK' in result)
def test_playlistsearch(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistsearch_without_quotes(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_plchanges(self): def test_plchanges(self):
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')]) [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: a' in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_plchanges_with_minus_one_returns_entire_playlist(self): 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')]) [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: a' in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_plchanges_without_quotes_works(self): 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')]) [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: a' in result)
self.assert_(u'Title: b' in result) self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result) self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_plchangesposid(self): def test_plchangesposid(self):
self.b.current_playlist.append([Track(), Track(), Track()]) self.backend.current_playlist.append([Track(), Track(), Track()])
result = self.h.handle_request(u'plchangesposid "0"') result = self.dispatcher.handle_request(u'plchangesposid "0"')
cp_tracks = self.b.current_playlist.cp_tracks.get() cp_tracks = self.backend.current_playlist.cp_tracks.get()
self.assert_(u'cpos: 0' in result) self.assert_(u'cpos: 0' in result)
self.assert_(u'Id: %d' % cp_tracks[0][0] self.assert_(u'Id: %d' % cp_tracks[0][0]
in result) in result)
@ -380,24 +381,24 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_shuffle_without_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
version = self.b.current_playlist.version.get() version = self.backend.current_playlist.version.get()
result = self.h.handle_request(u'shuffle') result = self.dispatcher.handle_request(u'shuffle')
self.assert_(version < self.b.current_playlist.version.get()) self.assert_(version < self.backend.current_playlist.version.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_shuffle_with_open_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
version = self.b.current_playlist.version.get() version = self.backend.current_playlist.version.get()
result = self.h.handle_request(u'shuffle "4:"') result = self.dispatcher.handle_request(u'shuffle "4:"')
self.assert_(version < self.b.current_playlist.version.get()) self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b') self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[2].name, 'c')
@ -405,14 +406,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_shuffle_with_closed_range(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
version = self.b.current_playlist.version.get() version = self.backend.current_playlist.version.get()
result = self.h.handle_request(u'shuffle "1:3"') result = self.dispatcher.handle_request(u'shuffle "1:3"')
self.assert_(version < self.b.current_playlist.version.get()) self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[4].name, 'e')
@ -420,12 +421,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_swap(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'swap "1" "4"') result = self.dispatcher.handle_request(u'swap "1" "4"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e') self.assertEqual(tracks[1].name, 'e')
self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[2].name, 'c')
@ -435,12 +436,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_swapid(self): 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='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'), Track(name='d'), Track(name='e'), Track(name='f'),
]) ])
result = self.h.handle_request(u'swapid "1" "4"') result = self.dispatcher.handle_request(u'swapid "1" "4"')
tracks = self.b.current_playlist.tracks.get() tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e') self.assertEqual(tracks[1].name, 'e')
self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[2].name, 'c')

View File

@ -1,33 +1,33 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.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 from mopidy.mixers.dummy import DummyMixer
class MpdDispatcherTest(unittest.TestCase): class MpdDispatcherTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_register_same_pattern_twice_fails(self): def test_register_same_pattern_twice_fails(self):
func = lambda: None func = lambda: None
try: try:
handle_pattern('a pattern')(func) handle_request('a pattern')(func)
handle_pattern('a pattern')(func) handle_request('a pattern')(func)
self.fail('Registering a pattern twice shoulde raise ValueError') self.fail('Registering a pattern twice shoulde raise ValueError')
except ValueError: except ValueError:
pass pass
def test_finding_handler_for_unknown_command_raises_exception(self): def test_finding_handler_for_unknown_command_raises_exception(self):
try: try:
self.h.find_handler('an_unknown_command with args') self.dispatcher._find_handler('an_unknown_command with args')
self.fail('Should raise exception') self.fail('Should raise exception')
except MpdAckError as e: except MpdAckError as e:
self.assertEqual(e.get_mpd_ack(), self.assertEqual(e.get_mpd_ack(),
@ -37,18 +37,18 @@ class MpdDispatcherTest(unittest.TestCase):
expected_handler = lambda x: None expected_handler = lambda x: None
request_handlers['known_command (?P<arg1>.+)'] = \ request_handlers['known_command (?P<arg1>.+)'] = \
expected_handler 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.assertEqual(handler, expected_handler)
self.assert_('arg1' in kwargs) self.assert_('arg1' in kwargs)
self.assertEqual(kwargs['arg1'], 'an_arg') self.assertEqual(kwargs['arg1'], 'an_arg')
def test_handling_unknown_request_yields_error(self): 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"') self.assertEqual(result[0], u'ACK [5@0] {} unknown command "an"')
def test_handling_known_request(self): def test_handling_known_request(self):
expected = 'magic' expected = 'magic'
request_handlers['known request'] = lambda x: expected 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_(u'OK' in result)
self.assert_(expected in result) self.assert_(expected in result)

View File

@ -1,7 +1,7 @@
import unittest import unittest
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand, from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
MpdNotImplemented) MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
class MpdExceptionsTest(unittest.TestCase): class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self): 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): def test_get_mpd_ack_with_values(self):
try: try:
raise MpdAckError('A description', error_code=6, index=7, raise MpdAckError('A description', index=7, command='foo')
command='foo')
except MpdAckError as e: 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): def test_mpd_unknown_command(self):
try: try:
@ -36,3 +35,17 @@ class MpdExceptionsTest(unittest.TestCase):
except MpdAckError as e: except MpdAckError as e:
self.assertEqual(e.get_mpd_ack(), self.assertEqual(e.get_mpd_ack(),
u'ACK [5@0] {} unknown command "play"') 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"')

View File

@ -1,390 +1,412 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_count(self): 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'songs: 0' in result)
self.assert_(u'playtime: 0' in result) self.assert_(u'playtime: 0' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_findadd(self): 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) self.assert_(u'OK' in result)
def test_listall(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_listallinfo(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_lsinfo_without_path_returns_same_as_listplaylists(self): def test_lsinfo_without_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo') lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
listplaylists_result = self.h.handle_request(u'listplaylists') listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result) self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo ""') lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
listplaylists_result = self.h.handle_request(u'listplaylists') listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result) self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_for_root_returns_same_as_listplaylists(self): def test_lsinfo_for_root_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo "/"') lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
listplaylists_result = self.h.handle_request(u'listplaylists') listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result) self.assertEqual(lsinfo_result, listplaylists_result)
def test_update_without_uri(self): 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'OK' in result)
self.assert_(u'updating_db: 0' in result) self.assert_(u'updating_db: 0' in result)
def test_update_with_uri(self): 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'OK' in result)
self.assert_(u'updating_db: 0' in result) self.assert_(u'updating_db: 0' in result)
def test_rescan_without_uri(self): 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'OK' in result)
self.assert_(u'updating_db: 0' in result) self.assert_(u'updating_db: 0' in result)
def test_rescan_with_uri(self): 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'OK' in result)
self.assert_(u'updating_db: 0' in result) self.assert_(u'updating_db: 0' in result)
class MusicDatabaseFindTest(unittest.TestCase): class MusicDatabaseFindTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_find_album(self): 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) self.assert_(u'OK' in result)
def test_find_album_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_find_artist(self): 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) self.assert_(u'OK' in result)
def test_find_artist_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_find_title(self): 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) self.assert_(u'OK' in result)
def test_find_title_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_find_date(self): 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) self.assert_(u'OK' in result)
def test_find_date_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_find_date_with_capital_d_and_incomplete_date(self): 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) self.assert_(u'OK' in result)
def test_find_else_should_fail(self): 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') self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
def test_find_album_and_artist(self): 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"') u'find album "album_what" artist "artist_what"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
class MusicDatabaseListTest(unittest.TestCase): class MusicDatabaseListTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_list_foo_returns_ack(self): 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], self.assertEqual(result[0],
u'ACK [2@0] {list} incorrect arguments') u'ACK [2@0] {list} incorrect arguments')
### Artist ### Artist
def test_list_artist_with_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_artist_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_artist_without_quotes_and_capitalized(self): 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) self.assert_(u'OK' in result)
def test_list_artist_with_query_of_one_token(self): 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], self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments') u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_artist_with_unknown_field_in_query_returns_ack(self): 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], self.assertEqual(result[0],
u'ACK [2@0] {list} not able to parse args') u'ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self): 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) self.assert_(u'OK' in result)
def test_list_artist_by_album(self): 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) self.assert_(u'OK' in result)
def test_list_artist_by_full_date(self): 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) self.assert_(u'OK' in result)
def test_list_artist_by_year(self): 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) self.assert_(u'OK' in result)
def test_list_artist_by_genre(self): 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) self.assert_(u'OK' in result)
def test_list_artist_by_artist_and_album(self): 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"') u'list "artist" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
### Album ### Album
def test_list_album_with_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_album_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_album_without_quotes_and_capitalized(self): 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) self.assert_(u'OK' in result)
def test_list_album_with_artist_name(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_artist(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_album(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_full_date(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_year(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_genre(self): 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) self.assert_(u'OK' in result)
def test_list_album_by_artist_and_album(self): 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"') u'list "album" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
### Date ### Date
def test_list_date_with_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_date_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_date_without_quotes_and_capitalized(self): 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) self.assert_(u'OK' in result)
def test_list_date_with_query_of_one_token(self): 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], self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments') u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self): 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) self.assert_(u'OK' in result)
def test_list_date_by_album(self): 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) self.assert_(u'OK' in result)
def test_list_date_by_full_date(self): 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) self.assert_(u'OK' in result)
def test_list_date_by_year(self): 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) self.assert_(u'OK' in result)
def test_list_date_by_genre(self): 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) self.assert_(u'OK' in result)
def test_list_date_by_artist_and_album(self): 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"') u'list "date" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
### Genre ### Genre
def test_list_genre_with_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_genre_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_list_genre_without_quotes_and_capitalized(self): 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) self.assert_(u'OK' in result)
def test_list_genre_with_query_of_one_token(self): 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], self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments') u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self): 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) self.assert_(u'OK' in result)
def test_list_genre_by_album(self): 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) self.assert_(u'OK' in result)
def test_list_genre_by_full_date(self): 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) self.assert_(u'OK' in result)
def test_list_genre_by_year(self): 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) self.assert_(u'OK' in result)
def test_list_genre_by_genre(self): 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) self.assert_(u'OK' in result)
def test_list_genre_by_artist_and_album(self): 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"') u'list "genre" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
class MusicDatabaseSearchTest(unittest.TestCase): class MusicDatabaseSearchTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_search_album(self): 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) self.assert_(u'OK' in result)
def test_search_album_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_artist(self): 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) self.assert_(u'OK' in result)
def test_search_artist_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_filename(self): 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) self.assert_(u'OK' in result)
def test_search_filename_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_title(self): 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) self.assert_(u'OK' in result)
def test_search_title_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_any(self): 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) self.assert_(u'OK' in result)
def test_search_any_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_date(self): 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) self.assert_(u'OK' in result)
def test_search_date_without_quotes(self): 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) self.assert_(u'OK' in result)
def test_search_date_with_capital_d_and_incomplete_date(self): 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) self.assert_(u'OK' in result)
def test_search_else_should_fail(self): 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') self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')

View File

@ -2,7 +2,7 @@ import unittest
from mopidy.backends.base import PlaybackController from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track from mopidy.models import Track
@ -14,393 +14,378 @@ STOPPED = PlaybackController.STOPPED
class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_consume_off(self): def test_consume_off(self):
result = self.h.handle_request(u'consume "0"') result = self.dispatcher.handle_request(u'consume "0"')
self.assertFalse(self.b.playback.consume.get()) self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_consume_off_without_quotes(self): def test_consume_off_without_quotes(self):
result = self.h.handle_request(u'consume 0') result = self.dispatcher.handle_request(u'consume 0')
self.assertFalse(self.b.playback.consume.get()) self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_consume_on(self): def test_consume_on(self):
result = self.h.handle_request(u'consume "1"') result = self.dispatcher.handle_request(u'consume "1"')
self.assertTrue(self.b.playback.consume.get()) self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_consume_on_without_quotes(self): def test_consume_on_without_quotes(self):
result = self.h.handle_request(u'consume 1') result = self.dispatcher.handle_request(u'consume 1')
self.assertTrue(self.b.playback.consume.get()) self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_crossfade(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_random_off(self): def test_random_off(self):
result = self.h.handle_request(u'random "0"') result = self.dispatcher.handle_request(u'random "0"')
self.assertFalse(self.b.playback.random.get()) self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_random_off_without_quotes(self): def test_random_off_without_quotes(self):
result = self.h.handle_request(u'random 0') result = self.dispatcher.handle_request(u'random 0')
self.assertFalse(self.b.playback.random.get()) self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_random_on(self): def test_random_on(self):
result = self.h.handle_request(u'random "1"') result = self.dispatcher.handle_request(u'random "1"')
self.assertTrue(self.b.playback.random.get()) self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_random_on_without_quotes(self): def test_random_on_without_quotes(self):
result = self.h.handle_request(u'random 1') result = self.dispatcher.handle_request(u'random 1')
self.assertTrue(self.b.playback.random.get()) self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_repeat_off(self): def test_repeat_off(self):
result = self.h.handle_request(u'repeat "0"') result = self.dispatcher.handle_request(u'repeat "0"')
self.assertFalse(self.b.playback.repeat.get()) self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_repeat_off_without_quotes(self): def test_repeat_off_without_quotes(self):
result = self.h.handle_request(u'repeat 0') result = self.dispatcher.handle_request(u'repeat 0')
self.assertFalse(self.b.playback.repeat.get()) self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_repeat_on(self): def test_repeat_on(self):
result = self.h.handle_request(u'repeat "1"') result = self.dispatcher.handle_request(u'repeat "1"')
self.assertTrue(self.b.playback.repeat.get()) self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_repeat_on_without_quotes(self): def test_repeat_on_without_quotes(self):
result = self.h.handle_request(u'repeat 1') result = self.dispatcher.handle_request(u'repeat 1')
self.assertTrue(self.b.playback.repeat.get()) self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_setvol_below_min(self): 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.assert_(u'OK' in result)
self.assertEqual(0, self.mixer.volume.get()) self.assertEqual(0, self.mixer.volume.get())
def test_setvol_min(self): 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.assert_(u'OK' in result)
self.assertEqual(0, self.mixer.volume.get()) self.assertEqual(0, self.mixer.volume.get())
def test_setvol_middle(self): 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.assert_(u'OK' in result)
self.assertEqual(50, self.mixer.volume.get()) self.assertEqual(50, self.mixer.volume.get())
def test_setvol_max(self): 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.assert_(u'OK' in result)
self.assertEqual(100, self.mixer.volume.get()) self.assertEqual(100, self.mixer.volume.get())
def test_setvol_above_max(self): 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.assert_(u'OK' in result)
self.assertEqual(100, self.mixer.volume.get()) self.assertEqual(100, self.mixer.volume.get())
def test_setvol_plus_is_ignored(self): 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.assert_(u'OK' in result)
self.assertEqual(10, self.mixer.volume.get()) self.assertEqual(10, self.mixer.volume.get())
def test_setvol_without_quotes(self): 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.assert_(u'OK' in result)
self.assertEqual(50, self.mixer.volume.get()) self.assertEqual(50, self.mixer.volume.get())
def test_single_off(self): def test_single_off(self):
result = self.h.handle_request(u'single "0"') result = self.dispatcher.handle_request(u'single "0"')
self.assertFalse(self.b.playback.single.get()) self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_single_off_without_quotes(self): def test_single_off_without_quotes(self):
result = self.h.handle_request(u'single 0') result = self.dispatcher.handle_request(u'single 0')
self.assertFalse(self.b.playback.single.get()) self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_single_on(self): def test_single_on(self):
result = self.h.handle_request(u'single "1"') result = self.dispatcher.handle_request(u'single "1"')
self.assertTrue(self.b.playback.single.get()) self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_single_on_without_quotes(self): def test_single_on_without_quotes(self):
result = self.h.handle_request(u'single 1') result = self.dispatcher.handle_request(u'single 1')
self.assertTrue(self.b.playback.single.get()) self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_replay_gain_mode_off(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_mode_track(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_mode_album(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_replay_gain_status_default(self): def test_replay_gain_status_default(self):
expected = u'off' 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_(u'OK' in result)
self.assert_(expected in result) self.assert_(expected in result)
def test_replay_gain_status_off(self): def test_replay_gain_status_off(self):
raise SkipTest raise SkipTest # TODO
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)
def test_replay_gain_status_track(self): def test_replay_gain_status_track(self):
raise SkipTest raise SkipTest # TODO
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)
def test_replay_gain_status_album(self): def test_replay_gain_status_album(self):
raise SkipTest raise SkipTest # TODO
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)
class PlaybackControlHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_next(self): def test_next(self):
result = self.h.handle_request(u'next') result = self.dispatcher.handle_request(u'next')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_pause_off(self): def test_pause_off(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.h.handle_request(u'play "0"') self.dispatcher.handle_request(u'play "0"')
self.h.handle_request(u'pause "1"') self.dispatcher.handle_request(u'pause "1"')
result = self.h.handle_request(u'pause "0"') result = self.dispatcher.handle_request(u'pause "0"')
self.assert_(u'OK' in result) 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): def test_pause_on(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.h.handle_request(u'play "0"') self.dispatcher.handle_request(u'play "0"')
result = self.h.handle_request(u'pause "1"') result = self.dispatcher.handle_request(u'pause "1"')
self.assert_(u'OK' in result) 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): def test_pause_toggle(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"') result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
result = self.h.handle_request(u'pause') result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PAUSED, self.b.playback.state.get()) self.assertEqual(PAUSED, self.backend.playback.state.get())
result = self.h.handle_request(u'pause') result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result) 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): def test_play_without_pos(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.b.playback.state = PAUSED self.backend.playback.state = PAUSED
result = self.h.handle_request(u'play') result = self.dispatcher.handle_request(u'play')
self.assert_(u'OK' in result) 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): def test_play_with_pos(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"') result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result) 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): def test_play_with_pos_without_quotes(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
result = self.h.handle_request(u'play 0') result = self.dispatcher.handle_request(u'play 0')
self.assert_(u'OK' in result) 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): def test_play_with_pos_out_of_bounds(self):
self.b.current_playlist.append([]) self.backend.current_playlist.append([])
result = self.h.handle_request(u'play "0"') result = self.dispatcher.handle_request(u'play "0"')
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') 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): def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'play "-1"') result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get().uri, 'a') self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self): 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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
self.b.playback.play() self.backend.playback.play()
self.b.playback.next() self.backend.playback.next()
self.b.playback.stop() self.backend.playback.stop()
self.assertNotEqual(self.b.playback.current_track.get(), None) self.assertNotEqual(self.backend.playback.current_track.get(), None)
result = self.h.handle_request(u'play "-1"') result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get().uri, 'b') self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
def test_play_minus_one_on_empty_playlist_does_not_ack(self): def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear() self.backend.current_playlist.clear()
result = self.h.handle_request(u'play "-1"') result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(STOPPED, self.b.playback.state.get()) self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
def test_play_minus_is_ignored_if_playing(self): def test_play_minus_is_ignored_if_playing(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000) self.backend.playback.seek(30000)
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.b.playback.state.get()) self.assertEquals(PLAYING, self.backend.playback.state.get())
result = self.h.handle_request(u'play "-1"') result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_play_minus_one_resumes_if_paused(self): def test_play_minus_one_resumes_if_paused(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000) self.backend.playback.seek(30000)
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.b.playback.state.get()) self.assertEquals(PLAYING, self.backend.playback.state.get())
self.b.playback.pause() self.backend.playback.pause()
self.assertEquals(PAUSED, self.b.playback.state.get()) self.assertEquals(PAUSED, self.backend.playback.state.get())
result = self.h.handle_request(u'play "-1"') result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid(self): def test_playid(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "0"') result = self.dispatcher.handle_request(u'playid "0"')
self.assert_(u'OK' in result) 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): def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'playid "-1"') result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get().uri, 'a') self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): 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.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
self.b.playback.play() self.backend.playback.play()
self.b.playback.next() self.backend.playback.next()
self.b.playback.stop() self.backend.playback.stop()
self.assertNotEqual(self.b.playback.current_track.get(), None) self.assertNotEqual(self.backend.playback.current_track.get(), None)
result = self.h.handle_request(u'playid "-1"') result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get().uri, 'b') self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self): def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear() self.backend.current_playlist.clear()
result = self.h.handle_request(u'playid "-1"') result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(STOPPED, self.b.playback.state.get()) self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertEqual(self.b.playback.current_track.get(), None) self.assertEqual(self.backend.playback.current_track.get(), None)
def test_playid_minus_is_ignored_if_playing(self): def test_playid_minus_is_ignored_if_playing(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000) self.backend.playback.seek(30000)
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.b.playback.state.get()) self.assertEquals(PLAYING, self.backend.playback.state.get())
result = self.h.handle_request(u'playid "-1"') result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid_minus_one_resumes_if_paused(self): def test_playid_minus_one_resumes_if_paused(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000) self.backend.playback.seek(30000)
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.b.playback.state.get()) self.assertEquals(PLAYING, self.backend.playback.state.get())
self.b.playback.pause() self.backend.playback.pause()
self.assertEquals(PAUSED, self.b.playback.state.get()) self.assertEquals(PAUSED, self.backend.playback.state.get())
result = self.h.handle_request(u'playid "-1"') result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
self.assertEqual(PLAYING, self.b.playback.state.get()) self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.b.playback.time_position.get() >= 30000) self.assert_(self.backend.playback.time_position.get() >= 30000)
def test_playid_which_does_not_exist(self): def test_playid_which_does_not_exist(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "12345"') result = self.dispatcher.handle_request(u'playid "12345"')
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song') self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
def test_previous(self): def test_previous(self):
result = self.h.handle_request(u'previous') result = self.dispatcher.handle_request(u'previous')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_seek(self): def test_seek(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.h.handle_request(u'seek "0"') self.dispatcher.handle_request(u'seek "0"')
result = self.h.handle_request(u'seek "0" "30"') result = self.dispatcher.handle_request(u'seek "0" "30"')
self.assert_(u'OK' in result) 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): def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000) seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(uri='1', length=40000), seek_track]) [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.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): def test_seek_without_quotes(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
self.h.handle_request(u'seek 0') self.dispatcher.handle_request(u'seek 0')
result = self.h.handle_request(u'seek 0 30') result = self.dispatcher.handle_request(u'seek 0 30')
self.assert_(u'OK' in result) 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): def test_seekid(self):
self.b.current_playlist.append([Track(length=40000)]) self.backend.current_playlist.append([Track(length=40000)])
result = self.h.handle_request(u'seekid "0" "30"') result = self.dispatcher.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result) 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): def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000) seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append( self.backend.current_playlist.append(
[Track(length=40000), seek_track]) [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.assert_(u'OK' in result)
self.assertEqual(self.b.playback.current_cpid.get(), 1) self.assertEqual(self.backend.playback.current_cpid.get(), 1)
self.assertEqual(self.b.playback.current_track.get(), seek_track) self.assertEqual(self.backend.playback.current_track.get(), seek_track)
def test_stop(self): 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.assert_(u'OK' in result)
self.assertEqual(STOPPED, self.b.playback.state.get()) self.assertEqual(STOPPED, self.backend.playback.state.get())

View File

@ -1,25 +1,29 @@
import unittest import unittest
from mopidy import settings
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase): class ReflectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() settings.runtime.clear()
self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_commands_returns_list_of_all_commands(self): 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 # Check if some random commands are included
self.assert_(u'command: commands' in result) self.assert_(u'command: commands' in result)
self.assert_(u'command: play' in result) self.assert_(u'command: play' in result)
self.assert_(u'command: status' 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 # Check if the blacklisted commands are not present
self.assert_(u'command: command_list_begin' not in result) self.assert_(u'command: command_list_begin' not in result)
self.assert_(u'command: command_list_ok_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'command: sticker' not in result)
self.assert_(u'OK' 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): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_notcommands_returns_only_ok(self): def test_notcommands_returns_only_kill_and_ok(self):
result = self.h.handle_request(u'notcommands') result = self.dispatcher.handle_request(u'notcommands')
self.assertEqual(1, len(result)) self.assertEqual(2, len(result))
self.assert_(u'command: kill' in result)
self.assert_(u'OK' 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): def test_tagtypes(self):
result = self.h.handle_request(u'tagtypes') result = self.dispatcher.handle_request(u'tagtypes')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_urlhandlers(self): 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'OK' in result)
self.assert_(u'handler: dummy:' in result) self.assert_(u'handler: dummy:' in result)

View File

@ -44,52 +44,3 @@ class MpdSessionTest(unittest.TestCase):
self.session.input_buffer = ['\xff'] self.session.input_buffer = ['\xff']
self.session.found_terminator() self.session.found_terminator()
self.assertEqual(len(self.session.input_buffer), 0) 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)

View File

@ -2,7 +2,8 @@ import unittest
from mopidy.backends.base import PlaybackController from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track from mopidy.models import Track
@ -12,23 +13,24 @@ STOPPED = PlaybackController.STOPPED
class StatusHandlerTest(unittest.TestCase): class StatusHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
self.context = self.dispatcher.context
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_clearerror(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_currentsong(self): def test_currentsong(self):
track = Track() track = Track()
self.b.current_playlist.append([track]) self.backend.current_playlist.append([track])
self.b.playback.play() self.backend.playback.play()
result = self.h.handle_request(u'currentsong') result = self.dispatcher.handle_request(u'currentsong')
self.assert_(u'file: ' in result) self.assert_(u'file: ' in result)
self.assert_(u'Time: 0' in result) self.assert_(u'Time: 0' in result)
self.assert_(u'Artist: ' in result) self.assert_(u'Artist: ' in result)
@ -41,27 +43,27 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_currentsong_without_song(self): 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) self.assert_(u'OK' in result)
def test_idle_without_subsystems(self): 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) self.assert_(u'OK' in result)
def test_idle_with_subsystems(self): 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) self.assert_(u'OK' in result)
def test_noidle(self): def test_noidle(self):
result = self.h.handle_request(u'noidle') result = self.dispatcher.handle_request(u'noidle')
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_stats_command(self): 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) self.assert_(u'OK' in result)
def test_stats_method(self): def test_stats_method(self):
result = dispatcher.status.stats(self.h) result = status.stats(self.context)
self.assert_('artists' in result) self.assert_('artists' in result)
self.assert_(int(result['artists']) >= 0) self.assert_(int(result['artists']) >= 0)
self.assert_('albums' in result) self.assert_('albums' in result)
@ -78,110 +80,110 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(int(result['playtime']) >= 0) self.assert_(int(result['playtime']) >= 0)
def test_status_command(self): 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) self.assert_(u'OK' in result)
def test_status_method_contains_volume_which_defaults_to_0(self): 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.assert_('volume' in result)
self.assertEqual(int(result['volume']), 0) self.assertEqual(int(result['volume']), 0)
def test_status_method_contains_volume(self): def test_status_method_contains_volume(self):
self.mixer.volume = 17 self.mixer.volume = 17
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('volume' in result) self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 17) self.assertEqual(int(result['volume']), 17)
def test_status_method_contains_repeat_is_0(self): 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.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 0) self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self): def test_status_method_contains_repeat_is_1(self):
self.b.playback.repeat = 1 self.backend.playback.repeat = 1
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('repeat' in result) self.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 1) self.assertEqual(int(result['repeat']), 1)
def test_status_method_contains_random_is_0(self): 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.assert_('random' in result)
self.assertEqual(int(result['random']), 0) self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self): def test_status_method_contains_random_is_1(self):
self.b.playback.random = 1 self.backend.playback.random = 1
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('random' in result) self.assert_('random' in result)
self.assertEqual(int(result['random']), 1) self.assertEqual(int(result['random']), 1)
def test_status_method_contains_single(self): 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_('single' in result)
self.assert_(int(result['single']) in (0, 1)) self.assert_(int(result['single']) in (0, 1))
def test_status_method_contains_consume_is_0(self): 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.assert_('consume' in result)
self.assertEqual(int(result['consume']), 0) self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self): def test_status_method_contains_consume_is_1(self):
self.b.playback.consume = 1 self.backend.playback.consume = 1
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('consume' in result) self.assert_('consume' in result)
self.assertEqual(int(result['consume']), 1) self.assertEqual(int(result['consume']), 1)
def test_status_method_contains_playlist(self): 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_('playlist' in result)
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1)) self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
def test_status_method_contains_playlistlength(self): 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_('playlistlength' in result)
self.assert_(int(result['playlistlength']) >= 0) self.assert_(int(result['playlistlength']) >= 0)
def test_status_method_contains_xfade(self): 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_('xfade' in result)
self.assert_(int(result['xfade']) >= 0) self.assert_(int(result['xfade']) >= 0)
def test_status_method_contains_state_is_play(self): def test_status_method_contains_state_is_play(self):
self.b.playback.state = PLAYING self.backend.playback.state = PLAYING
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assert_('state' in result)
self.assertEqual(result['state'], 'play') self.assertEqual(result['state'], 'play')
def test_status_method_contains_state_is_stop(self): def test_status_method_contains_state_is_stop(self):
self.b.playback.state = STOPPED self.backend.playback.state = STOPPED
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assert_('state' in result)
self.assertEqual(result['state'], 'stop') self.assertEqual(result['state'], 'stop')
def test_status_method_contains_state_is_pause(self): def test_status_method_contains_state_is_pause(self):
self.b.playback.state = PLAYING self.backend.playback.state = PLAYING
self.b.playback.state = PAUSED self.backend.playback.state = PAUSED
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assert_('state' in result)
self.assertEqual(result['state'], 'pause') self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self): def test_status_method_when_playlist_loaded_contains_song(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.b.playback.play() self.backend.playback.play()
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('song' in result) self.assert_('song' in result)
self.assert_(int(result['song']) >= 0) self.assert_(int(result['song']) >= 0)
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
self.b.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.b.playback.play() self.backend.playback.play()
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('songid' in result) self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 0) self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self): def test_status_method_when_playing_contains_time_with_no_length(self):
self.b.current_playlist.append([Track(length=None)]) self.backend.current_playlist.append([Track(length=None)])
self.b.playback.play() self.backend.playback.play()
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('time' in result) self.assert_('time' in result)
(position, total) = result['time'].split(':') (position, total) = result['time'].split(':')
position = int(position) position = int(position)
@ -189,9 +191,9 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total) self.assert_(position <= total)
def test_status_method_when_playing_contains_time_with_length(self): def test_status_method_when_playing_contains_time_with_length(self):
self.b.current_playlist.append([Track(length=10000)]) self.backend.current_playlist.append([Track(length=10000)])
self.b.playback.play() self.backend.playback.play()
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('time' in result) self.assert_('time' in result)
(position, total) = result['time'].split(':') (position, total) = result['time'].split(':')
position = int(position) position = int(position)
@ -199,15 +201,15 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total) self.assert_(position <= total)
def test_status_method_when_playing_contains_elapsed(self): def test_status_method_when_playing_contains_elapsed(self):
self.b.playback.state = PAUSED self.backend.playback.state = PAUSED
self.b.playback.play_time_accumulated = 59123 self.backend.playback.play_time_accumulated = 59123
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('elapsed' in result) self.assert_('elapsed' in result)
self.assertEqual(int(result['elapsed']), 59123) self.assertEqual(int(result['elapsed']), 59123)
def test_status_method_when_playing_contains_bitrate(self): def test_status_method_when_playing_contains_bitrate(self):
self.b.current_playlist.append([Track(bitrate=320)]) self.backend.current_playlist.append([Track(bitrate=320)])
self.b.playback.play() self.backend.playback.play()
result = dict(dispatcher.status.status(self.h)) result = dict(status.status(self.context))
self.assert_('bitrate' in result) self.assert_('bitrate' in result)
self.assertEqual(int(result['bitrate']), 320) self.assertEqual(int(result['bitrate']), 320)

View File

@ -1,45 +1,45 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase): class StickersHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_sticker_get(self): def test_sticker_get(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'sticker get "song" "file:///dev/urandom" "a_name"') u'sticker get "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_set(self): 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"') u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_with_name(self): 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"') u'sticker delete "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_without_name(self): def test_sticker_delete_without_name(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'sticker delete "song" "file:///dev/urandom"') u'sticker delete "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_list(self): def test_sticker_list(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'sticker list "song" "file:///dev/urandom"') u'sticker list "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_find(self): def test_sticker_find(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'sticker find "song" "file:///dev/urandom" "a_name"') u'sticker find "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)

View File

@ -2,64 +2,64 @@ import datetime as dt
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist from mopidy.models import Track, Playlist
class StoredPlaylistsHandlerTest(unittest.TestCase): class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend.start().proxy() self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy() self.mixer = DummyMixer.start().proxy()
self.h = dispatcher.MpdDispatcher() self.dispatcher = MpdDispatcher()
def tearDown(self): def tearDown(self):
self.b.stop().get() self.backend.stop().get()
self.mixer.stop().get() self.mixer.stop().get()
def test_listplaylist(self): def test_listplaylist(self):
self.b.stored_playlists.playlists = [ self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] 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'file: file:///dev/urandom' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_listplaylist_fails_if_no_playlist_is_found(self): 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], self.assertEqual(result[0],
u'ACK [50@0] {listplaylist} No such playlist') u'ACK [50@0] {listplaylist} No such playlist')
def test_listplaylistinfo(self): def test_listplaylistinfo(self):
self.b.stored_playlists.playlists = [ self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] 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'file: file:///dev/urandom' in result)
self.assert_(u'Track: 0' in result) self.assert_(u'Track: 0' in result)
self.assert_(u'Pos: 0' not in result) self.assert_(u'Pos: 0' not in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_listplaylistinfo_fails_if_no_playlist_is_found(self): 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], self.assertEqual(result[0],
u'ACK [50@0] {listplaylistinfo} No such playlist') u'ACK [50@0] {listplaylistinfo} No such playlist')
def test_listplaylists(self): def test_listplaylists(self):
last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345) 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)] 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) self.assert_(u'playlist: a' in result)
# Date without microseconds and with time zone information # Date without microseconds and with time zone information
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result) self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_load_known_playlist_appends_to_current_playlist(self): def test_load_known_playlist_appends_to_current_playlist(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.b.stored_playlists.playlists = [Playlist(name='A-list', self.backend.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] 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) 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(len(tracks), 5)
self.assertEqual(tracks[0].uri, 'a') self.assertEqual(tracks[0].uri, 'a')
self.assertEqual(tracks[1].uri, 'b') self.assertEqual(tracks[1].uri, 'b')
@ -68,35 +68,35 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
self.assertEqual(tracks[4].uri, 'e') self.assertEqual(tracks[4].uri, 'e')
def test_load_unknown_playlist_acks(self): 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.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): def test_playlistadd(self):
result = self.h.handle_request( result = self.dispatcher.handle_request(
u'playlistadd "name" "file:///dev/urandom"') u'playlistadd "name" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistclear(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistdelete(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistmove(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rename(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rm(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_save(self): 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) self.assert_(u'ACK [0@0] {} Not implemented' in result)

View File

@ -1,7 +1,7 @@
import datetime as dt import datetime as dt
import unittest import unittest
from mopidy.models import Artist, Album, Track, Playlist from mopidy.models import Artist, Album, CpTrack, Track, Playlist
from tests import SkipTest from tests import SkipTest
@ -274,6 +274,21 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(hash(album1), hash(album2)) 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): class TrackTest(unittest.TestCase):
def test_uri(self): def test_uri(self):
uri = u'an_uri' uri = u'an_uri'

View File

@ -2,8 +2,8 @@ import os
import unittest import unittest
from mopidy import settings as default_settings_module, SettingsError from mopidy import settings as default_settings_module, SettingsError
from mopidy.utils.settings import validate_settings, SettingsProxy from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
from mopidy.utils.settings import mask_value_if_secret SettingsProxy, validate_settings)
class ValidateSettingsTest(unittest.TestCase): class ValidateSettingsTest(unittest.TestCase):
def setUp(self): def setUp(self):
@ -140,3 +140,48 @@ class SettingsProxyTest(unittest.TestCase):
self.settings.TEST = './test' self.settings.TEST = './test'
actual = self.settings.TEST actual = self.settings.TEST
self.assertEqual(actual, './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)