diff --git a/docs/api/mpd.rst b/docs/api/mpd.rst index 7bf7fe7b..6361e909 100644 --- a/docs/api/mpd.rst +++ b/docs/api/mpd.rst @@ -4,6 +4,8 @@ .. automodule:: mopidy.frontends.mpd :synopsis: MPD frontend + :members: + :undoc-members: MPD server @@ -17,10 +19,21 @@ MPD server .. inheritance-diagram:: mopidy.frontends.mpd.server -MPD frontend -============ +MPD session +=========== -.. automodule:: mopidy.frontends.mpd.frontend +.. automodule:: mopidy.frontends.mpd.session + :synopsis: MPD client session + :members: + :undoc-members: + +.. inheritance-diagram:: mopidy.frontends.mpd.session + + +MPD dispatcher +============== + +.. automodule:: mopidy.frontends.mpd.dispatcher :synopsis: MPD request dispatcher :members: :undoc-members: diff --git a/docs/changes.rst b/docs/changes.rst index 341ef850..7c2ce19d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,6 +58,9 @@ greatly improved MPD client support. - Support for single track repeat added. (Fixes: :issue:`4`) - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. + - Remove setting :attr:`mopidy.settings.SERVER` and + :attr:`mopidy.settings.FRONTEND` in favour of the new + :attr:`mopidy.settings.FRONTENDS`. - Backends: diff --git a/mopidy/__init__.py b/mopidy/__init__.py index e3321041..7cdcad6a 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -5,9 +5,6 @@ if not (2, 6) <= sys.version_info < (3,): def get_version(): return u'0.1.0a4' -def get_mpd_protocol_version(): - return u'0.16.0' - class MopidyException(Exception): def __init__(self, message, *args, **kwargs): super(MopidyException, self).__init__(message, *args, **kwargs) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index a2230180..1c0318e7 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -24,11 +24,11 @@ def main(): logger.info('-- Starting Mopidy --') get_or_create_folder('~/.mopidy/') core_queue = multiprocessing.Queue() - get_class(settings.SERVER)(core_queue).start() output_class = get_class(settings.OUTPUT) backend_class = get_class(settings.BACKENDS[0]) - frontend_class = get_class(settings.FRONTEND) - core = CoreProcess(core_queue, output_class, backend_class, frontend_class) + frontend = get_class(settings.FRONTENDS[0])() + frontend.start_server(core_queue) + core = CoreProcess(core_queue, output_class, backend_class, frontend) core.start() asyncore.loop() diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 83d6ce4c..53f2003f 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,93 +1,33 @@ -import re +from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.frontends.mpd.server import MpdServer -from mopidy import MopidyException - -class MpdAckError(MopidyException): +class MpdFrontend(object): """ - Available MPD error codes:: - - ACK_ERROR_NOT_LIST = 1 - ACK_ERROR_ARG = 2 - ACK_ERROR_PASSWORD = 3 - ACK_ERROR_PERMISSION = 4 - ACK_ERROR_UNKNOWN = 5 - ACK_ERROR_NO_EXIST = 50 - ACK_ERROR_PLAYLIST_MAX = 51 - ACK_ERROR_SYSTEM = 52 - ACK_ERROR_PLAYLIST_LOAD = 53 - ACK_ERROR_UPDATE_ALREADY = 54 - ACK_ERROR_PLAYER_SYNC = 55 - ACK_ERROR_EXIST = 56 + The MPD frontend. """ - def __init__(self, message=u'', error_code=0, index=0, command=u''): - super(MpdAckError, self).__init__(message, error_code, index, command) - self.message = message - self.error_code = error_code - self.index = index - self.command = command + def __init__(self): + self.server = None + self.dispatcher = None - def get_mpd_ack(self): + def start_server(self, core_queue): """ - MPD error code format:: + Starts the MPD server. - ACK [%(error_code)i@%(index)i] {%(command)s} description + :param core_queue: the core queue + :type core_queue: :class:`multiprocessing.Queue` """ - return u'ACK [%i@%i] {%s} %s' % ( - self.error_code, self.index, self.command, self.message) + self.server = MpdServer(core_queue) + self.server.start() -class MpdArgError(MpdAckError): - def __init__(self, *args, **kwargs): - super(MpdArgError, self).__init__(*args, **kwargs) - self.error_code = 2 # ACK_ERROR_ARG + def create_dispatcher(self, backend): + """ + Creates a dispatcher for MPD requests. -class MpdUnknownCommand(MpdAckError): - def __init__(self, *args, **kwargs): - super(MpdUnknownCommand, self).__init__(*args, **kwargs) - self.message = u'unknown command "%s"' % self.command - self.command = u'' - self.error_code = 5 # ACK_ERROR_UNKNOWN + :param backend: the backend + :type backend: :class:`mopidy.backends.base.BaseBackend` + :rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher` + """ -class MpdNoExistError(MpdAckError): - def __init__(self, *args, **kwargs): - super(MpdNoExistError, self).__init__(*args, **kwargs) - self.error_code = 50 # ACK_ERROR_NO_EXIST - -class MpdNotImplemented(MpdAckError): - def __init__(self, *args, **kwargs): - super(MpdNotImplemented, self).__init__(*args, **kwargs) - self.message = u'Not implemented' - -mpd_commands = set() -request_handlers = {} - -def handle_pattern(pattern): - """ - Decorator for connecting command handlers to command patterns. - - 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 - argument a default value. - - For example, if the command is ``do that thing`` the ``what`` argument will - be ``this thing``:: - - @handle_pattern('^do (?P.+)$') - def do(what): - ... - - :param pattern: regexp pattern for matching commands - :type pattern: string - """ - def decorator(func): - match = re.search('([a-z_]+)', pattern) - if match is not None: - mpd_commands.add(match.group()) - if pattern in request_handlers: - raise ValueError(u'Tried to redefine handler for %s with %s' % ( - pattern, func)) - request_handlers[pattern] = func - func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % ( - pattern, func.__doc__ or '') - return func - return decorator + self.dispatcher = MpdDispatcher(backend) + return self.dispatcher diff --git a/mopidy/frontends/mpd/frontend.py b/mopidy/frontends/mpd/dispatcher.py similarity index 82% rename from mopidy/frontends/mpd/frontend.py rename to mopidy/frontends/mpd/dispatcher.py index 9a0251eb..2a477e1c 100644 --- a/mopidy/frontends/mpd/frontend.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -1,20 +1,18 @@ -import logging import re -from mopidy.frontends.mpd import (mpd_commands, request_handlers, - handle_pattern, MpdAckError, MpdArgError, MpdUnknownCommand) +from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, + MpdUnknownCommand) +from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers # Do not remove the following import. The protocol modules must be imported to # get them registered as request handlers. from mopidy.frontends.mpd.protocol import (audio_output, command_list, - connection, current_playlist, music_db, playback, reflection, status, - stickers, stored_playlists) + connection, current_playlist, empty, music_db, playback, reflection, + status, stickers, stored_playlists) from mopidy.utils import flatten -logger = logging.getLogger('mopidy.frontends.mpd.frontend') - -class MpdFrontend(object): +class MpdDispatcher(object): """ - The MPD frontend dispatches MPD requests to the correct handler. + Dispatches MPD requests to the correct handler. """ def __init__(self, backend=None): @@ -72,8 +70,3 @@ class MpdFrontend(object): if add_ok and (not response or not response[-1].startswith(u'ACK')): response.append(u'OK') return response - -@handle_pattern(r'^$') -def empty(frontend): - """The original MPD server returns ``OK`` on an empty request.""" - pass diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py new file mode 100644 index 00000000..2a18b2f3 --- /dev/null +++ b/mopidy/frontends/mpd/exceptions.py @@ -0,0 +1,57 @@ +from mopidy import MopidyException + +class MpdAckError(MopidyException): + """ + Available MPD error codes:: + + ACK_ERROR_NOT_LIST = 1 + ACK_ERROR_ARG = 2 + ACK_ERROR_PASSWORD = 3 + ACK_ERROR_PERMISSION = 4 + ACK_ERROR_UNKNOWN = 5 + ACK_ERROR_NO_EXIST = 50 + ACK_ERROR_PLAYLIST_MAX = 51 + ACK_ERROR_SYSTEM = 52 + ACK_ERROR_PLAYLIST_LOAD = 53 + ACK_ERROR_UPDATE_ALREADY = 54 + ACK_ERROR_PLAYER_SYNC = 55 + ACK_ERROR_EXIST = 56 + """ + + def __init__(self, message=u'', error_code=0, index=0, command=u''): + super(MpdAckError, self).__init__(message, error_code, index, command) + self.message = message + self.error_code = error_code + self.index = index + self.command = command + + def get_mpd_ack(self): + """ + MPD error code format:: + + ACK [%(error_code)i@%(index)i] {%(command)s} description + """ + return u'ACK [%i@%i] {%s} %s' % ( + self.error_code, self.index, self.command, self.message) + +class MpdArgError(MpdAckError): + def __init__(self, *args, **kwargs): + super(MpdArgError, self).__init__(*args, **kwargs) + self.error_code = 2 # ACK_ERROR_ARG + +class MpdUnknownCommand(MpdAckError): + def __init__(self, *args, **kwargs): + super(MpdUnknownCommand, self).__init__(*args, **kwargs) + self.message = u'unknown command "%s"' % self.command + self.command = u'' + self.error_code = 5 # ACK_ERROR_UNKNOWN + +class MpdNoExistError(MpdAckError): + def __init__(self, *args, **kwargs): + super(MpdNoExistError, self).__init__(*args, **kwargs) + self.error_code = 50 # ACK_ERROR_NO_EXIST + +class MpdNotImplemented(MpdAckError): + def __init__(self, *args, **kwargs): + super(MpdNotImplemented, self).__init__(*args, **kwargs) + self.message = u'Not implemented' diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 00932e90..756aa3c3 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -10,8 +10,47 @@ implement our own MPD server which is compatible with the numerous existing `MPD clients `_. """ +import re + #: The MPD protocol uses UTF-8 for encoding all data. ENCODING = u'utf-8' #: The MPD protocol uses ``\n`` as line terminator. LINE_TERMINATOR = u'\n' + +#: The MPD protocol version is 0.16.0. +VERSION = u'0.16.0' + +mpd_commands = set() +request_handlers = {} + +def handle_pattern(pattern): + """ + Decorator for connecting command handlers to command patterns. + + 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 + argument a default value. + + For example, if the command is ``do that thing`` the ``what`` argument will + be ``this thing``:: + + @handle_pattern('^do (?P.+)$') + def do(what): + ... + + :param pattern: regexp pattern for matching commands + :type pattern: string + """ + def decorator(func): + match = re.search('([a-z_]+)', pattern) + if match is not None: + mpd_commands.add(match.group()) + if pattern in request_handlers: + raise ValueError(u'Tried to redefine handler for %s with %s' % ( + pattern, func)) + request_handlers[pattern] = func + func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % ( + pattern, func.__doc__ or '') + return func + return decorator diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py index e659b162..d25fc118 100644 --- a/mopidy/frontends/mpd/protocol/audio_output.py +++ b/mopidy/frontends/mpd/protocol/audio_output.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^disableoutput "(?P\d+)"$') def disableoutput(frontend, outputid): diff --git a/mopidy/frontends/mpd/protocol/command_list.py b/mopidy/frontends/mpd/protocol/command_list.py index 900c26b0..b3df0be6 100644 --- a/mopidy/frontends/mpd/protocol/command_list.py +++ b/mopidy/frontends/mpd/protocol/command_list.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import handle_pattern, MpdUnknownCommand +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdUnknownCommand @handle_pattern(r'^command_list_begin$') def command_list_begin(frontend): diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py index 4312ded5..0ce3ef51 100644 --- a/mopidy/frontends/mpd/protocol/connection.py +++ b/mopidy/frontends/mpd/protocol/connection.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^close$') def close(frontend): @@ -9,8 +10,7 @@ def close(frontend): Closes the connection to MPD. """ - # TODO Does not work after multiprocessing branch merge - #frontend.session.do_close() + pass # TODO @handle_pattern(r'^kill$') def kill(frontend): @@ -21,8 +21,7 @@ def kill(frontend): Kills MPD. """ - # TODO Does not work after multiprocessing branch merge - #frontend.session.do_kill() + pass # TODO @handle_pattern(r'^password "(?P[^"]+)"$') def password_(frontend, password): diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index b9111d9e..90a53f5f 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError, +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @handle_pattern(r'^add "(?P[^"]*)"$') diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py new file mode 100644 index 00000000..a39d79eb --- /dev/null +++ b/mopidy/frontends/mpd/protocol/empty.py @@ -0,0 +1,6 @@ +from mopidy.frontends.mpd.protocol import handle_pattern + +@handle_pattern(r'^$') +def empty(frontend): + """The original MPD server returns ``OK`` on an empty request.""" + pass diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 5aec6eae..d4dcf50d 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -1,7 +1,7 @@ import re -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented -from mopidy.frontends.mpd.protocol import stored_playlists +from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists +from mopidy.frontends.mpd.exceptions import MpdNotImplemented def _build_query(mpd_query): """ diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 4c602f3b..c3fbdd5f 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError, +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @handle_pattern(r'^consume (?P[01])$') diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 0c349746..d2c9c599 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -1,5 +1,5 @@ -from mopidy.frontends.mpd import (handle_pattern, mpd_commands, - MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^commands$') def commands(frontend): diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 16e73dea..e18f1ea4 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^clearerror$') def clearerror(frontend): diff --git a/mopidy/frontends/mpd/protocol/stickers.py b/mopidy/frontends/mpd/protocol/stickers.py index c184d1f9..145665eb 100644 --- a/mopidy/frontends/mpd/protocol/stickers.py +++ b/mopidy/frontends/mpd/protocol/stickers.py @@ -1,4 +1,5 @@ -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^sticker delete "(?P[^"]+)" ' r'"(?P[^"]+)"( "(?P[^"]+)")*$') diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index 3d7a8710..c34b1676 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -1,7 +1,7 @@ import datetime as dt -from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError, - MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented @handle_pattern(r'^listplaylist "(?P[^"]+)"$') def listplaylist(frontend, name): diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 91d8e67a..db13e516 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -1,21 +1,18 @@ -import asynchat import asyncore import logging -import multiprocessing import re import socket import sys -from mopidy import get_mpd_protocol_version, settings -from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR -from mopidy.process import pickle_connection -from mopidy.utils import indent +from mopidy import settings +from .session import MpdSession logger = logging.getLogger('mopidy.frontends.mpd.server') class MpdServer(asyncore.dispatcher): """ - The MPD server. Creates a :class:`MpdSession` for each client connection. + The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession` + for each client connection. """ def __init__(self, core_queue): @@ -58,65 +55,3 @@ class MpdServer(asyncore.dispatcher): and re.match('\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % hostname return hostname - - -class MpdSession(asynchat.async_chat): - """ - The MPD client session. Keeps track of a single client and dispatches its - MPD requests to the frontend. - """ - - def __init__(self, server, client_socket, client_socket_address, - core_queue): - asynchat.async_chat.__init__(self, sock=client_socket) - self.server = server - self.client_address = client_socket_address[0] - self.client_port = client_socket_address[1] - self.core_queue = core_queue - self.input_buffer = [] - self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) - - def start(self): - """Start a new client session.""" - self.send_response(u'OK MPD %s' % get_mpd_protocol_version()) - - def collect_incoming_data(self, data): - """Collect incoming data into buffer until a terminator is found.""" - self.input_buffer.append(data) - - def found_terminator(self): - """Handle request when a terminator is found.""" - data = ''.join(self.input_buffer).strip() - self.input_buffer = [] - try: - request = data.decode(ENCODING) - logger.debug(u'Input from [%s]:%s: %s', self.client_address, - self.client_port, indent(request)) - self.handle_request(request) - except UnicodeDecodeError as e: - logger.warning(u'Received invalid data: %s', e) - - def handle_request(self, request): - """Handle request by sending it to the MPD frontend.""" - my_end, other_end = multiprocessing.Pipe() - self.core_queue.put({ - 'command': 'mpd_request', - 'request': request, - 'reply_to': pickle_connection(other_end), - }) - my_end.poll(None) - response = my_end.recv() - if response is not None: - self.handle_response(response) - - def handle_response(self, response): - """Handle response from the MPD frontend.""" - self.send_response(LINE_TERMINATOR.join(response)) - - def send_response(self, output): - """Send a response to the client.""" - logger.debug(u'Output to [%s]:%s: %s', self.client_address, - self.client_port, indent(output)) - output = u'%s%s' % (output, LINE_TERMINATOR) - data = output.encode(ENCODING) - self.push(data) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py new file mode 100644 index 00000000..e5d2d12d --- /dev/null +++ b/mopidy/frontends/mpd/session.py @@ -0,0 +1,70 @@ +import asynchat +import logging +import multiprocessing + +from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION +from mopidy.process import pickle_connection +from mopidy.utils import indent + +logger = logging.getLogger('mopidy.frontends.mpd.session') + +class MpdSession(asynchat.async_chat): + """ + The MPD client session. Keeps track of a single client and passes its + MPD requests to the dispatcher. + """ + + def __init__(self, server, client_socket, client_socket_address, + core_queue): + asynchat.async_chat.__init__(self, sock=client_socket) + self.server = server + self.client_address = client_socket_address[0] + self.client_port = client_socket_address[1] + self.core_queue = core_queue + self.input_buffer = [] + self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) + + def start(self): + """Start a new client session.""" + self.send_response(u'OK MPD %s' % VERSION) + + def collect_incoming_data(self, data): + """Collect incoming data into buffer until a terminator is found.""" + self.input_buffer.append(data) + + def found_terminator(self): + """Handle request when a terminator is found.""" + data = ''.join(self.input_buffer).strip() + self.input_buffer = [] + try: + request = data.decode(ENCODING) + logger.debug(u'Input from [%s]:%s: %s', self.client_address, + self.client_port, indent(request)) + self.handle_request(request) + except UnicodeDecodeError as e: + logger.warning(u'Received invalid data: %s', e) + + def handle_request(self, request): + """Handle request by sending it to the MPD frontend.""" + my_end, other_end = multiprocessing.Pipe() + self.core_queue.put({ + 'command': 'mpd_request', + 'request': request, + 'reply_to': pickle_connection(other_end), + }) + my_end.poll(None) + response = my_end.recv() + if response is not None: + self.handle_response(response) + + def handle_response(self, response): + """Handle response from the MPD frontend.""" + self.send_response(LINE_TERMINATOR.join(response)) + + def send_response(self, output): + """Send a response to the client.""" + logger.debug(u'Output to [%s]:%s: %s', self.client_address, + self.client_port, indent(output)) + output = u'%s%s' % (output, LINE_TERMINATOR) + data = output.encode(ENCODING) + self.push(data) diff --git a/mopidy/process.py b/mopidy/process.py index 5ad1b0b4..008129b3 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -39,17 +39,16 @@ class BaseProcess(multiprocessing.Process): class CoreProcess(BaseProcess): - def __init__(self, core_queue, output_class, backend_class, - frontend_class): + def __init__(self, core_queue, output_class, backend_class, frontend): super(CoreProcess, self).__init__() self.core_queue = core_queue self.output_queue = None self.output_class = output_class self.backend_class = backend_class - self.frontend_class = frontend_class self.output = None self.backend = None - self.frontend = None + self.frontend = frontend + self.dispatcher = None def run_inside_try(self): self.setup() @@ -61,13 +60,13 @@ class CoreProcess(BaseProcess): self.output_queue = multiprocessing.Queue() self.output = self.output_class(self.core_queue, self.output_queue) self.backend = self.backend_class(self.core_queue, self.output_queue) - self.frontend = self.frontend_class(self.backend) + self.dispatcher = self.frontend.create_dispatcher(self.backend) def process_message(self, message): if message.get('to') == 'output': self.output_queue.put(message) elif message['command'] == 'mpd_request': - response = self.frontend.handle_request(message['request']) + response = self.dispatcher.handle_request(message['request']) connection = unpickle_connection(message['reply_to']) connection.send(response) elif message['command'] == 'end_of_track': diff --git a/mopidy/settings.py b/mopidy/settings.py index c9e3606e..8a0ae862 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -41,12 +41,15 @@ DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT #: DUMP_LOG_FILENAME = u'dump.log' DUMP_LOG_FILENAME = u'dump.log' -#: Protocol frontend to use. +#: List of server frontends to use. #: #: Default:: #: -#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' -FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' +#: FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',) +#: +#: .. note:: +#: Currently only the first frontend in the list is used. +FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',) #: Path to folder with local music. #: @@ -127,13 +130,6 @@ MIXER_MAX_VOLUME = 100 #: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' -#: Server to use. -#: -#: Default:: -#: -#: SERVER = u'mopidy.frontends.mpd.server.MpdServer' -SERVER = u'mopidy.frontends.mpd.server.MpdServer' - #: Which address Mopidy's MPD server should bind to. #: #:Examples: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 478a03e6..f1d213de 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -37,7 +37,7 @@ class SettingsProxy(object): def current(self): current = copy(self.default) current.update(self.local) - return current + return current def __getattr__(self, attr): if not self._is_setting(attr): @@ -81,6 +81,8 @@ def validate_settings(defaults, settings): errors = {} changed = { + 'FRONTEND': 'FRONTENDS', + 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT', 'SPOTIFY_LIB_APPKEY': None, diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index 24201341..b81e727e 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py index 683a1013..6c801c3f 100644 --- a/tests/frontends/mpd/command_list_test.py +++ b/tests/frontends/mpd/command_list_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class CommandListsTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_command_list_begin(self): result = self.h.handle_request(u'command_list_begin') diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 341e630c..21753054 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_close(self): result = self.h.handle_request(u'close') diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index e27e58c5..c53e2b8d 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -1,14 +1,14 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_add(self): needle = Track(uri='dummy://foo') diff --git a/tests/frontends/mpd/request_handler_test.py b/tests/frontends/mpd/dispatcher_test.py similarity index 75% rename from tests/frontends/mpd/request_handler_test.py rename to tests/frontends/mpd/dispatcher_test.py index ac8bd7e9..2a2ee4db 100644 --- a/tests/frontends/mpd/request_handler_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -1,19 +1,21 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend, MpdAckError +from mopidy.frontends.mpd import dispatcher +from mopidy.frontends.mpd.exceptions import MpdAckError +from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern from mopidy.mixers.dummy import DummyMixer -class RequestHandlerTest(unittest.TestCase): +class MpdDispatcherTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_register_same_pattern_twice_fails(self): func = lambda: None try: - frontend.handle_pattern('a pattern')(func) - frontend.handle_pattern('a pattern')(func) + handle_pattern('a pattern')(func) + handle_pattern('a pattern')(func) self.fail('Registering a pattern twice shoulde raise ValueError') except ValueError: pass @@ -28,7 +30,7 @@ class RequestHandlerTest(unittest.TestCase): def test_finding_handler_for_known_command_returns_handler_and_kwargs(self): expected_handler = lambda x: None - frontend.request_handlers['known_command (?P.+)'] = \ + request_handlers['known_command (?P.+)'] = \ expected_handler (handler, kwargs) = self.h.find_handler('known_command an_arg') self.assertEqual(handler, expected_handler) @@ -41,7 +43,7 @@ class RequestHandlerTest(unittest.TestCase): def test_handling_known_request(self): expected = 'magic' - frontend.request_handlers['known request'] = lambda x: expected + request_handlers['known request'] = lambda x: expected result = self.h.handle_request('known request') self.assert_(u'OK' in result) self.assert_(expected in result) diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py index e337550f..ef222d46 100644 --- a/tests/frontends/mpd/exception_test.py +++ b/tests/frontends/mpd/exception_test.py @@ -1,6 +1,6 @@ import unittest -from mopidy.frontends.mpd import (MpdAckError, MpdUnknownCommand, +from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand, MpdNotImplemented) class MpdExceptionsTest(unittest.TestCase): diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index fc8f980a..5fcc393c 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_count(self): result = self.h.handle_request(u'count "tag" "needle"') diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 17263aef..3ba48a54 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -1,14 +1,14 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_consume_off(self): result = self.h.handle_request(u'consume "0"') @@ -167,7 +167,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_next(self): result = self.h.handle_request(u'next') diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index 5491946c..a4491d75 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class ReflectionHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_commands_returns_list_of_all_commands(self): result = self.h.handle_request(u'commands') diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 9839acfe..fbd0ff9e 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,14 +1,14 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class StatusHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_clearerror(self): result = self.h.handle_request(u'clearerror') @@ -51,7 +51,7 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_stats_method(self): - result = frontend.status.stats(self.h) + result = dispatcher.status.stats(self.h) self.assert_('artists' in result) self.assert_(int(result['artists']) >= 0) self.assert_('albums' in result) @@ -72,106 +72,106 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_status_method_contains_volume_which_defaults_to_0(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 0) def test_status_method_contains_volume(self): self.b.mixer.volume = 17 - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 17) def test_status_method_contains_repeat_is_0(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('repeat' in result) self.assertEqual(int(result['repeat']), 0) def test_status_method_contains_repeat_is_1(self): self.b.playback.repeat = 1 - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('repeat' in result) self.assertEqual(int(result['repeat']), 1) def test_status_method_contains_random_is_0(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('random' in result) self.assertEqual(int(result['random']), 0) def test_status_method_contains_random_is_1(self): self.b.playback.random = 1 - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('random' in result) self.assertEqual(int(result['random']), 1) def test_status_method_contains_single(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('single' in result) self.assert_(int(result['single']) in (0, 1)) def test_status_method_contains_consume_is_0(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('consume' in result) self.assertEqual(int(result['consume']), 0) def test_status_method_contains_consume_is_1(self): self.b.playback.consume = 1 - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('consume' in result) self.assertEqual(int(result['consume']), 1) def test_status_method_contains_playlist(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('playlist' in result) self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1)) def test_status_method_contains_playlistlength(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('playlistlength' in result) self.assert_(int(result['playlistlength']) >= 0) def test_status_method_contains_xfade(self): - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('xfade' in result) self.assert_(int(result['xfade']) >= 0) def test_status_method_contains_state_is_play(self): self.b.playback.state = self.b.playback.PLAYING - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): self.b.playback.state = self.b.playback.STOPPED - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): self.b.playback.state = self.b.playback.PLAYING self.b.playback.state = self.b.playback.PAUSED - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): self.b.current_playlist.append([Track()]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('song' in result) self.assert_(int(result['song']) >= 0) def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): self.b.current_playlist.append([Track()]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('songid' in result) self.assertEqual(int(result['songid']), 1) def test_status_method_when_playing_contains_time_with_no_length(self): self.b.current_playlist.append([Track(length=None)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('time' in result) (position, total) = result['time'].split(':') position = int(position) @@ -181,7 +181,7 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_time_with_length(self): self.b.current_playlist.append([Track(length=10000)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('time' in result) (position, total) = result['time'].split(':') position = int(position) @@ -191,13 +191,13 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_elapsed(self): self.b.playback.state = self.b.playback.PAUSED self.b.playback._play_time_accumulated = 59123 - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('elapsed' in result) self.assertEqual(int(result['elapsed']), 59123) def test_status_method_when_playing_contains_bitrate(self): self.b.current_playlist.append([Track(bitrate=320)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('bitrate' in result) self.assertEqual(int(result['bitrate']), 320) diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py index 401eaf57..5b66d723 100644 --- a/tests/frontends/mpd/stickers_test.py +++ b/tests/frontends/mpd/stickers_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class StickersHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_sticker_get(self): result = self.h.handle_request( diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index 6e5717af..a24cbb88 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -2,14 +2,14 @@ import datetime as dt import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track, Playlist class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_listplaylist(self): self.b.stored_playlists.playlists = [