Source code for mopidy.mpd.protocol

"""
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 <https://mpd.fandom.com/>`_ using telnet to get the details we need to
implement our own MPD server which is compatible with the numerous existing
`MPD clients <https://mpd.fandom.com/wiki/Clients>`_.
"""

from __future__ import absolute_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.19.0.
VERSION = '0.19.0'


[docs]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, mount, music_db, playback, reflection, status, stickers, stored_playlists)
[docs]def INT(value): # noqa: N802 r"""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)
[docs]def UINT(value): # noqa: N802 r"""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)
[docs]def BOOL(value): # noqa: N802 """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)
[docs]def RANGE(value): # noqa: N802 """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)
[docs]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?
[docs] 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) try: callargs = inspect.getcallargs(func, *args, **kwargs) except TypeError: raise exceptions.MpdArgError( 'wrong number of arguments for "%s"' % name) 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
[docs] 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()