Merge branch 'mpd-cleanup' into develop

This commit is contained in:
Stein Magnus Jodal 2010-08-20 00:48:20 +02:00
commit bc8d32275e
36 changed files with 316 additions and 259 deletions

View File

@ -4,6 +4,8 @@
.. automodule:: mopidy.frontends.mpd .. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend :synopsis: MPD frontend
:members:
:undoc-members:
MPD server MPD server
@ -17,10 +19,21 @@ MPD server
.. inheritance-diagram:: mopidy.frontends.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 :synopsis: MPD request dispatcher
:members: :members:
:undoc-members: :undoc-members:

View File

@ -58,6 +58,9 @@ greatly improved MPD client support.
- Support for single track repeat added. (Fixes: :issue:`4`) - Support for single track repeat added. (Fixes: :issue:`4`)
- Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming
in backends. in backends.
- Remove setting :attr:`mopidy.settings.SERVER` and
:attr:`mopidy.settings.FRONTEND` in favour of the new
:attr:`mopidy.settings.FRONTENDS`.
- Backends: - Backends:

View File

@ -5,9 +5,6 @@ if not (2, 6) <= sys.version_info < (3,):
def get_version(): def get_version():
return u'0.1.0a4' return u'0.1.0a4'
def get_mpd_protocol_version():
return u'0.16.0'
class MopidyException(Exception): class MopidyException(Exception):
def __init__(self, message, *args, **kwargs): def __init__(self, message, *args, **kwargs):
super(MopidyException, self).__init__(message, *args, **kwargs) super(MopidyException, self).__init__(message, *args, **kwargs)

View File

@ -24,11 +24,11 @@ def main():
logger.info('-- Starting Mopidy --') logger.info('-- Starting Mopidy --')
get_or_create_folder('~/.mopidy/') get_or_create_folder('~/.mopidy/')
core_queue = multiprocessing.Queue() core_queue = multiprocessing.Queue()
get_class(settings.SERVER)(core_queue).start()
output_class = get_class(settings.OUTPUT) output_class = get_class(settings.OUTPUT)
backend_class = get_class(settings.BACKENDS[0]) backend_class = get_class(settings.BACKENDS[0])
frontend_class = get_class(settings.FRONTEND) frontend = get_class(settings.FRONTENDS[0])()
core = CoreProcess(core_queue, output_class, backend_class, frontend_class) frontend.start_server(core_queue)
core = CoreProcess(core_queue, output_class, backend_class, frontend)
core.start() core.start()
asyncore.loop() asyncore.loop()

View File

@ -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 MpdFrontend(object):
class MpdAckError(MopidyException):
""" """
Available MPD error codes:: The MPD frontend.
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''): def __init__(self):
super(MpdAckError, self).__init__(message, error_code, index, command) self.server = None
self.message = message self.dispatcher = None
self.error_code = error_code
self.index = index
self.command = command
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.server = MpdServer(core_queue)
self.error_code, self.index, self.command, self.message) self.server.start()
class MpdArgError(MpdAckError): def create_dispatcher(self, backend):
def __init__(self, *args, **kwargs): """
super(MpdArgError, self).__init__(*args, **kwargs) Creates a dispatcher for MPD requests.
self.error_code = 2 # ACK_ERROR_ARG
class MpdUnknownCommand(MpdAckError): :param backend: the backend
def __init__(self, *args, **kwargs): :type backend: :class:`mopidy.backends.base.BaseBackend`
super(MpdUnknownCommand, self).__init__(*args, **kwargs) :rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher`
self.message = u'unknown command "%s"' % self.command """
self.command = u''
self.error_code = 5 # ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError): self.dispatcher = MpdDispatcher(backend)
def __init__(self, *args, **kwargs): return self.dispatcher
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<what>.+)$')
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

View File

@ -1,20 +1,18 @@
import logging
import re import re
from mopidy.frontends.mpd import (mpd_commands, request_handlers, from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
handle_pattern, MpdAckError, MpdArgError, MpdUnknownCommand) MpdUnknownCommand)
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.
from mopidy.frontends.mpd.protocol import (audio_output, command_list, from mopidy.frontends.mpd.protocol import (audio_output, command_list,
connection, current_playlist, music_db, playback, reflection, status, connection, current_playlist, empty, music_db, playback, reflection,
stickers, stored_playlists) status, stickers, stored_playlists)
from mopidy.utils import flatten from mopidy.utils import flatten
logger = logging.getLogger('mopidy.frontends.mpd.frontend') class MpdDispatcher(object):
class MpdFrontend(object):
""" """
The MPD frontend dispatches MPD requests to the correct handler. Dispatches MPD requests to the correct handler.
""" """
def __init__(self, backend=None): 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')): if add_ok and (not response or not response[-1].startswith(u'ACK')):
response.append(u'OK') response.append(u'OK')
return response return response
@handle_pattern(r'^$')
def empty(frontend):
"""The original MPD server returns ``OK`` on an empty request."""
pass

View File

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

View File

@ -10,8 +10,47 @@ 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>`_.
""" """
import re
#: The MPD protocol uses UTF-8 for encoding all data. #: The MPD protocol uses UTF-8 for encoding all data.
ENCODING = u'utf-8' ENCODING = u'utf-8'
#: The MPD protocol uses ``\n`` as line terminator. #: The MPD protocol uses ``\n`` as line terminator.
LINE_TERMINATOR = u'\n' 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<what>.+)$')
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

View File

@ -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<outputid>\d+)"$') @handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
def disableoutput(frontend, outputid): def disableoutput(frontend, outputid):

View File

@ -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$') @handle_pattern(r'^command_list_begin$')
def command_list_begin(frontend): def command_list_begin(frontend):

View File

@ -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$') @handle_pattern(r'^close$')
def close(frontend): def close(frontend):
@ -9,8 +10,7 @@ def close(frontend):
Closes the connection to MPD. Closes the connection to MPD.
""" """
# TODO Does not work after multiprocessing branch merge pass # TODO
#frontend.session.do_close()
@handle_pattern(r'^kill$') @handle_pattern(r'^kill$')
def kill(frontend): def kill(frontend):
@ -21,8 +21,7 @@ def kill(frontend):
Kills MPD. Kills MPD.
""" """
# TODO Does not work after multiprocessing branch merge pass # TODO
#frontend.session.do_kill()
@handle_pattern(r'^password "(?P<password>[^"]+)"$') @handle_pattern(r'^password "(?P<password>[^"]+)"$')
def password_(frontend, password): def password_(frontend, password):

View File

@ -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) MpdNotImplemented)
@handle_pattern(r'^add "(?P<uri>[^"]*)"$') @handle_pattern(r'^add "(?P<uri>[^"]*)"$')

View File

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

View File

@ -1,7 +1,7 @@
import re import re
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
from mopidy.frontends.mpd.protocol import stored_playlists from mopidy.frontends.mpd.exceptions import MpdNotImplemented
def _build_query(mpd_query): def _build_query(mpd_query):
""" """

View File

@ -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) MpdNotImplemented)
@handle_pattern(r'^consume (?P<state>[01])$') @handle_pattern(r'^consume (?P<state>[01])$')

View File

@ -1,5 +1,5 @@
from mopidy.frontends.mpd import (handle_pattern, mpd_commands, from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands
MpdNotImplemented) from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^commands$') @handle_pattern(r'^commands$')
def commands(frontend): def commands(frontend):

View File

@ -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$') @handle_pattern(r'^clearerror$')
def clearerror(frontend): def clearerror(frontend):

View File

@ -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<field>[^"]+)" ' @handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$') r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')

View File

@ -1,7 +1,7 @@
import datetime as dt import datetime as dt
from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError, from mopidy.frontends.mpd.protocol import handle_pattern
MpdNotImplemented) from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$') @handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
def listplaylist(frontend, name): def listplaylist(frontend, name):

View File

@ -1,21 +1,18 @@
import asynchat
import asyncore import asyncore
import logging import logging
import multiprocessing
import re import re
import socket import socket
import sys import sys
from mopidy import get_mpd_protocol_version, settings from mopidy import settings
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR from .session import MpdSession
from mopidy.process import pickle_connection
from mopidy.utils import indent
logger = logging.getLogger('mopidy.frontends.mpd.server') logger = logging.getLogger('mopidy.frontends.mpd.server')
class MpdServer(asyncore.dispatcher): 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): def __init__(self, core_queue):
@ -58,65 +55,3 @@ class MpdServer(asyncore.dispatcher):
and re.match('\d+.\d+.\d+.\d+', hostname) is not None): and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname hostname = '::ffff:%s' % hostname
return 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)

View File

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

View File

@ -39,17 +39,16 @@ class BaseProcess(multiprocessing.Process):
class CoreProcess(BaseProcess): class CoreProcess(BaseProcess):
def __init__(self, core_queue, output_class, backend_class, def __init__(self, core_queue, output_class, backend_class, frontend):
frontend_class):
super(CoreProcess, self).__init__() super(CoreProcess, self).__init__()
self.core_queue = core_queue self.core_queue = core_queue
self.output_queue = None self.output_queue = None
self.output_class = output_class self.output_class = output_class
self.backend_class = backend_class self.backend_class = backend_class
self.frontend_class = frontend_class
self.output = None self.output = None
self.backend = None self.backend = None
self.frontend = None self.frontend = frontend
self.dispatcher = None
def run_inside_try(self): def run_inside_try(self):
self.setup() self.setup()
@ -61,13 +60,13 @@ class CoreProcess(BaseProcess):
self.output_queue = multiprocessing.Queue() self.output_queue = multiprocessing.Queue()
self.output = self.output_class(self.core_queue, self.output_queue) self.output = self.output_class(self.core_queue, self.output_queue)
self.backend = self.backend_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): def process_message(self, message):
if message.get('to') == 'output': if message.get('to') == 'output':
self.output_queue.put(message) self.output_queue.put(message)
elif message['command'] == 'mpd_request': 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 = unpickle_connection(message['reply_to'])
connection.send(response) connection.send(response)
elif message['command'] == 'end_of_track': elif message['command'] == 'end_of_track':

View File

@ -41,12 +41,15 @@ DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT
#: DUMP_LOG_FILENAME = u'dump.log' #: DUMP_LOG_FILENAME = u'dump.log'
DUMP_LOG_FILENAME = u'dump.log' DUMP_LOG_FILENAME = u'dump.log'
#: Protocol frontend to use. #: List of server frontends to use.
#: #:
#: Default:: #: Default::
#: #:
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' #: FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',)
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' #:
#: .. note::
#: Currently only the first frontend in the list is used.
FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',)
#: Path to folder with local music. #: 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'
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. #: Which address Mopidy's MPD server should bind to.
#: #:
#:Examples: #:Examples:

View File

@ -37,7 +37,7 @@ class SettingsProxy(object):
def current(self): def current(self):
current = copy(self.default) current = copy(self.default)
current.update(self.local) current.update(self.local)
return current return current
def __getattr__(self, attr): def __getattr__(self, attr):
if not self._is_setting(attr): if not self._is_setting(attr):
@ -81,6 +81,8 @@ def validate_settings(defaults, settings):
errors = {} errors = {}
changed = { changed = {
'FRONTEND': 'FRONTENDS',
'SERVER': None,
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT', 'SERVER_PORT': 'MPD_SERVER_PORT',
'SPOTIFY_LIB_APPKEY': None, 'SPOTIFY_LIB_APPKEY': None,

View File

@ -1,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase): class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_enableoutput(self): def test_enableoutput(self):
result = self.h.handle_request(u'enableoutput "0"') result = self.h.handle_request(u'enableoutput "0"')

View File

@ -1,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class CommandListsTest(unittest.TestCase): class CommandListsTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) 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): def test_command_list_begin(self):
result = self.h.handle_request(u'command_list_begin') result = self.h.handle_request(u'command_list_begin')

View File

@ -1,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase): class ConnectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_close(self): def test_close(self):
result = self.h.handle_request(u'close') result = self.h.handle_request(u'close')

View File

@ -1,14 +1,14 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.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(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_add(self): def test_add(self):
needle = Track(uri='dummy://foo') needle = Track(uri='dummy://foo')

View File

@ -1,19 +1,21 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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 from mopidy.mixers.dummy import DummyMixer
class RequestHandlerTest(unittest.TestCase): class MpdDispatcherTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) 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): def test_register_same_pattern_twice_fails(self):
func = lambda: None func = lambda: None
try: try:
frontend.handle_pattern('a pattern')(func) handle_pattern('a pattern')(func)
frontend.handle_pattern('a pattern')(func) handle_pattern('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
@ -28,7 +30,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self): def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
expected_handler = lambda x: None expected_handler = lambda x: None
frontend.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.h.find_handler('known_command an_arg')
self.assertEqual(handler, expected_handler) self.assertEqual(handler, expected_handler)
@ -41,7 +43,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_handling_known_request(self): def test_handling_known_request(self):
expected = 'magic' expected = 'magic'
frontend.request_handlers['known request'] = lambda x: expected request_handlers['known request'] = lambda x: expected
result = self.h.handle_request('known request') result = self.h.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,6 +1,6 @@
import unittest import unittest
from mopidy.frontends.mpd import (MpdAckError, MpdUnknownCommand, from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand,
MpdNotImplemented) MpdNotImplemented)
class MpdExceptionsTest(unittest.TestCase): class MpdExceptionsTest(unittest.TestCase):

View File

@ -1,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_count(self): def test_count(self):
result = self.h.handle_request(u'count "tag" "needle"') result = self.h.handle_request(u'count "tag" "needle"')

View File

@ -1,14 +1,14 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track from mopidy.models import Track
class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) 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): def test_consume_off(self):
result = self.h.handle_request(u'consume "0"') result = self.h.handle_request(u'consume "0"')
@ -167,7 +167,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
class PlaybackControlHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_next(self): def test_next(self):
result = self.h.handle_request(u'next') result = self.h.handle_request(u'next')

View File

@ -1,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase): class ReflectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) 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): def test_commands_returns_list_of_all_commands(self):
result = self.h.handle_request(u'commands') result = self.h.handle_request(u'commands')

View File

@ -1,14 +1,14 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
from mopidy.models import Track from mopidy.models import Track
class StatusHandlerTest(unittest.TestCase): class StatusHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_clearerror(self): def test_clearerror(self):
result = self.h.handle_request(u'clearerror') result = self.h.handle_request(u'clearerror')
@ -51,7 +51,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_stats_method(self): def test_stats_method(self):
result = frontend.status.stats(self.h) result = dispatcher.status.stats(self.h)
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)
@ -72,106 +72,106 @@ class StatusHandlerTest(unittest.TestCase):
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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.b.mixer.volume = 17 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.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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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.b.playback.repeat = 1
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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.b.playback.random = 1
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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.b.playback.consume = 1
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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 = self.b.playback.PLAYING 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.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 = self.b.playback.STOPPED 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.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 = self.b.playback.PLAYING self.b.playback.state = self.b.playback.PLAYING
self.b.playback.state = self.b.playback.PAUSED 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.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.b.current_playlist.append([Track()])
self.b.playback.play() 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_('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.b.current_playlist.append([Track()])
self.b.playback.play() self.b.playback.play()
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
self.assert_('songid' in result) self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 1) self.assertEqual(int(result['songid']), 1)
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.b.current_playlist.append([Track(length=None)])
self.b.playback.play() self.b.playback.play()
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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)
@ -181,7 +181,7 @@ class StatusHandlerTest(unittest.TestCase):
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.b.current_playlist.append([Track(length=10000)])
self.b.playback.play() self.b.playback.play()
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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)
@ -191,13 +191,13 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playing_contains_elapsed(self): def test_status_method_when_playing_contains_elapsed(self):
self.b.playback.state = self.b.playback.PAUSED self.b.playback.state = self.b.playback.PAUSED
self.b.playback._play_time_accumulated = 59123 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.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.b.current_playlist.append([Track(bitrate=320)])
self.b.playback.play() self.b.playback.play()
result = dict(frontend.status.status(self.h)) result = dict(dispatcher.status.status(self.h))
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,13 +1,13 @@
import unittest import unittest
from mopidy.backends.dummy import DummyBackend 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.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase): class StickersHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer) 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): def test_sticker_get(self):
result = self.h.handle_request( result = self.h.handle_request(

View File

@ -2,14 +2,14 @@ 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 frontend from mopidy.frontends.mpd import dispatcher
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(mixer_class=DummyMixer) self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b) self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_listplaylist(self): def test_listplaylist(self):
self.b.stored_playlists.playlists = [ self.b.stored_playlists.playlists = [