176 lines
5.9 KiB
Python
176 lines
5.9 KiB
Python
"""
|
|
This is Mopidy's MPD protocol implementation.
|
|
|
|
This is partly based upon the `MPD protocol documentation
|
|
<http://www.musicpd.org/doc/protocol/>`_, which is a useful resource, but it is
|
|
rather incomplete with regards to data formats, both for requests and
|
|
responses. Thus, we have had to talk a great deal with the the original `MPD
|
|
server <http://mpd.wikia.com/>`_ using telnet to get the details we need to
|
|
implement our own MPD server which is compatible with the numerous existing
|
|
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import inspect
|
|
|
|
from mopidy.mpd import exceptions
|
|
|
|
#: The MPD protocol uses UTF-8 for encoding all data.
|
|
ENCODING = 'UTF-8'
|
|
|
|
#: The MPD protocol uses ``\n`` as line terminator.
|
|
LINE_TERMINATOR = '\n'
|
|
|
|
#: The MPD protocol version is 0.17.0.
|
|
VERSION = '0.17.0'
|
|
|
|
|
|
def load_protocol_modules():
|
|
"""
|
|
The protocol modules must be imported to get them registered in
|
|
:attr:`commands`.
|
|
"""
|
|
from . import ( # noqa
|
|
audio_output, channels, command_list, connection, current_playlist,
|
|
music_db, playback, reflection, status, stickers, stored_playlists)
|
|
|
|
|
|
def INT(value):
|
|
"""Converts a value that matches [+-]?\d+ into and integer."""
|
|
if value is None:
|
|
raise ValueError('None is not a valid integer')
|
|
# TODO: check for whitespace via value != value.strip()?
|
|
return int(value)
|
|
|
|
|
|
def UINT(value):
|
|
"""Converts a value that matches \d+ into an integer."""
|
|
if value is None:
|
|
raise ValueError('None is not a valid integer')
|
|
if not value.isdigit():
|
|
raise ValueError('Only positive numbers are allowed')
|
|
return int(value)
|
|
|
|
|
|
def BOOL(value):
|
|
"""Convert the values 0 and 1 into booleans."""
|
|
if value in ('1', '0'):
|
|
return bool(int(value))
|
|
raise ValueError('%r is not 0 or 1' % value)
|
|
|
|
|
|
def RANGE(value):
|
|
"""Convert a single integer or range spec into a slice
|
|
|
|
``n`` should become ``slice(n, n+1)``
|
|
``n:`` should become ``slice(n, None)``
|
|
``n:m`` should become ``slice(n, m)`` and ``m > n`` must hold
|
|
"""
|
|
if ':' in value:
|
|
start, stop = value.split(':', 1)
|
|
start = UINT(start)
|
|
if stop.strip():
|
|
stop = UINT(stop)
|
|
if start >= stop:
|
|
raise ValueError('End must be larger than start')
|
|
else:
|
|
stop = None
|
|
else:
|
|
start = UINT(value)
|
|
stop = start + 1
|
|
return slice(start, stop)
|
|
|
|
|
|
class Commands(object):
|
|
"""Collection of MPD commands to expose to users.
|
|
|
|
Normally used through the global instance which command handlers have been
|
|
installed into.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.handlers = {}
|
|
|
|
# TODO: consider removing auth_required and list_command in favour of
|
|
# additional command instances to register in?
|
|
def add(self, name, auth_required=True, list_command=True, **validators):
|
|
"""Create a decorator that registers a handler and validation rules.
|
|
|
|
Additional keyword arguments are treated as converters/validators to
|
|
apply to tokens converting them to proper Python types.
|
|
|
|
Requirements for valid handlers:
|
|
|
|
- must accept a context argument as the first arg.
|
|
- may not use variable keyword arguments, ``**kwargs``.
|
|
- may use variable arguments ``*args`` *or* a mix of required and
|
|
optional arguments.
|
|
|
|
Decorator returns the unwrapped function so that tests etc can use the
|
|
functions with values with correct python types instead of strings.
|
|
|
|
:param string name: Name of the command being registered.
|
|
:param bool auth_required: If authorization is required.
|
|
:param bool list_command: If command should be listed in reflection.
|
|
"""
|
|
|
|
def wrapper(func):
|
|
if name in self.handlers:
|
|
raise ValueError('%s already registered' % name)
|
|
|
|
args, varargs, keywords, defaults = inspect.getargspec(func)
|
|
defaults = dict(zip(args[-len(defaults or []):], defaults or []))
|
|
|
|
if not args and not varargs:
|
|
raise TypeError('Handler must accept at least one argument.')
|
|
|
|
if len(args) > 1 and varargs:
|
|
raise TypeError(
|
|
'*args may not be combined with regular arguments')
|
|
|
|
if not set(validators.keys()).issubset(args):
|
|
raise TypeError('Validator for non-existent arg passed')
|
|
|
|
if keywords:
|
|
raise TypeError('**kwargs are not permitted')
|
|
|
|
def validate(*args, **kwargs):
|
|
if varargs:
|
|
return func(*args, **kwargs)
|
|
callargs = inspect.getcallargs(func, *args, **kwargs)
|
|
for key, value in callargs.items():
|
|
default = defaults.get(key, object())
|
|
if key in validators and value != default:
|
|
try:
|
|
callargs[key] = validators[key](value)
|
|
except ValueError:
|
|
raise exceptions.MpdArgError('incorrect arguments')
|
|
return func(**callargs)
|
|
|
|
validate.auth_required = auth_required
|
|
validate.list_command = list_command
|
|
self.handlers[name] = validate
|
|
return func
|
|
return wrapper
|
|
|
|
def call(self, tokens, context=None):
|
|
"""Find and run the handler registered for the given command.
|
|
|
|
If the handler was registered with any converters/validators they will
|
|
be run before calling the real handler.
|
|
|
|
:param list tokens: List of tokens to process
|
|
:param context: MPD context.
|
|
:type context: :class:`~mopidy.mpd.dispatcher.MpdContext`
|
|
"""
|
|
if not tokens:
|
|
raise exceptions.MpdNoCommand()
|
|
if tokens[0] not in self.handlers:
|
|
raise exceptions.MpdUnknownCommand(command=tokens[0])
|
|
return self.handlers[tokens[0]](context, *tokens[1:])
|
|
|
|
|
|
#: Global instance to install commands into
|
|
commands = Commands()
|