Source code for mopidy.mpd.dispatcher

from __future__ import unicode_literals

import logging
import re

import pykka

from mopidy.mpd import exceptions, protocol

logger = logging.getLogger(__name__)

protocol.load_protocol_modules()


[docs]class MpdDispatcher(object): """ The MPD session feeds the MPD dispatcher with requests. The dispatcher finds the correct handler, processes the request and sends the response back to the MPD session. """ _noidle = re.compile(r'^noidle$') def __init__(self, session=None, config=None, core=None): self.config = config self.authenticated = False self.command_list_receiving = False self.command_list_ok = False self.command_list = [] self.command_list_index = None self.context = MpdContext( self, session=session, config=config, core=core)
[docs] def handle_request(self, request, current_command_list_index=None): """Dispatch incoming requests to the correct handler.""" self.command_list_index = current_command_list_index response = [] filter_chain = [ self._catch_mpd_ack_errors_filter, self._authenticate_filter, self._command_list_filter, self._idle_filter, self._add_ok_filter, self._call_handler_filter, ] return self._call_next_filter(request, response, filter_chain)
def handle_idle(self, subsystem): self.context.events.add(subsystem) subsystems = self.context.subscriptions.intersection( self.context.events) if not subsystems: return response = [] for subsystem in subsystems: response.append('changed: %s' % subsystem) response.append('OK') self.context.subscriptions = set() self.context.events = set() self.context.session.send_lines(response) def _call_next_filter(self, request, response, filter_chain): if filter_chain: next_filter = filter_chain.pop(0) return next_filter(request, response, filter_chain) else: return response ### Filter: catch MPD ACK errors def _catch_mpd_ack_errors_filter(self, request, response, filter_chain): try: return self._call_next_filter(request, response, filter_chain) except exceptions.MpdAckError as mpd_ack_error: if self.command_list_index is not None: mpd_ack_error.index = self.command_list_index return [mpd_ack_error.get_mpd_ack()] ### Filter: authenticate def _authenticate_filter(self, request, response, filter_chain): if self.authenticated: return self._call_next_filter(request, response, filter_chain) elif self.config['mpd']['password'] is None: self.authenticated = True return self._call_next_filter(request, response, filter_chain) else: command_name = request.split(' ')[0] command_names_not_requiring_auth = [ command.name for command in protocol.mpd_commands if not command.auth_required] if command_name in command_names_not_requiring_auth: return self._call_next_filter(request, response, filter_chain) else: raise exceptions.MpdPermissionError(command=command_name) ### Filter: command list def _command_list_filter(self, request, response, filter_chain): if self._is_receiving_command_list(request): self.command_list.append(request) return [] else: response = self._call_next_filter(request, response, filter_chain) if (self._is_receiving_command_list(request) or self._is_processing_command_list(request)): if response and response[-1] == 'OK': response = response[:-1] return response def _is_receiving_command_list(self, request): return ( self.command_list_receiving and request != 'command_list_end') def _is_processing_command_list(self, request): return ( self.command_list_index is not None and request != 'command_list_end') ### Filter: idle def _idle_filter(self, request, response, filter_chain): if self._is_currently_idle() and not self._noidle.match(request): logger.debug( 'Client sent us %s, only %s is allowed while in ' 'the idle state', repr(request), repr('noidle')) self.context.session.close() return [] if not self._is_currently_idle() and self._noidle.match(request): return [] # noidle was called before idle response = self._call_next_filter(request, response, filter_chain) if self._is_currently_idle(): return [] else: return response def _is_currently_idle(self): return bool(self.context.subscriptions) ### Filter: add OK def _add_ok_filter(self, request, response, filter_chain): response = self._call_next_filter(request, response, filter_chain) if not self._has_error(response): response.append('OK') return response def _has_error(self, response): return response and response[-1].startswith('ACK') ### Filter: call handler def _call_handler_filter(self, request, response, filter_chain): try: response = self._format_response(self._call_handler(request)) return self._call_next_filter(request, response, filter_chain) except pykka.ActorDeadError as e: logger.warning('Tried to communicate with dead actor.') raise exceptions.MpdSystemError(e) def _call_handler(self, request): (handler, kwargs) = self._find_handler(request) try: return handler(self.context, **kwargs) except exceptions.MpdAckError as exc: if exc.command is None: exc.command = handler.__name__.split('__', 1)[0] raise def _find_handler(self, request): for pattern in protocol.request_handlers: matches = re.match(pattern, request) if matches is not None: return ( protocol.request_handlers[pattern], matches.groupdict()) command_name = request.split(' ')[0] if command_name in [command.name for command in protocol.mpd_commands]: raise exceptions.MpdArgError( 'incorrect arguments', command=command_name) raise exceptions.MpdUnknownCommand(command=command_name) def _format_response(self, response): formatted_response = [] for element in self._listify_result(response): formatted_response.extend(self._format_lines(element)) return formatted_response def _listify_result(self, result): if result is None: return [] if isinstance(result, set): return self._flatten(list(result)) if not isinstance(result, list): return [result] return self._flatten(result) def _flatten(self, the_list): result = [] for element in the_list: if isinstance(element, list): result.extend(self._flatten(element)) else: result.append(element) return result def _format_lines(self, line): if isinstance(line, dict): return ['%s: %s' % (key, value) for (key, value) in line.items()] if isinstance(line, tuple): (key, value) = line return ['%s: %s' % (key, value)] return [line]
[docs]class MpdContext(object): """ This object is passed as the first argument to all MPD command handlers to give the command handlers access to important parts of Mopidy. """ #: The current :class:`MpdDispatcher`. dispatcher = None #: The current :class:`mopidy.mpd.MpdSession`. session = None #: The MPD password password = None #: The Mopidy core API. An instance of :class:`mopidy.core.Core`. core = None #: The active subsystems that have pending events. events = None #: The subsytems that we want to be notified about in idle mode. subscriptions = None _invalid_playlist_chars = re.compile(r'[\n\r/]') def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session if config is not None: self.password = config['mpd']['password'] self.core = core self.events = set() self.subscriptions = set() self._playlist_uri_from_name = {} self._playlist_name_from_uri = {} self.refresh_playlists_mapping() def create_unique_name(self, playlist_name): stripped_name = self._invalid_playlist_chars.sub(' ', playlist_name) name = stripped_name i = 2 while name in self._playlist_uri_from_name: name = '%s [%d]' % (stripped_name, i) i += 1 return name
[docs] def refresh_playlists_mapping(self): """ Maintain map between playlists and unique playlist names to be used by MPD """ if self.core is not None: self._playlist_uri_from_name.clear() self._playlist_name_from_uri.clear() for playlist in self.core.playlists.playlists.get(): if not playlist.name: continue # TODO: add scheme to name perhaps 'foo (spotify)' etc. name = self.create_unique_name(playlist.name) self._playlist_uri_from_name[name] = playlist.uri self._playlist_name_from_uri[playlist.uri] = name
[docs] def lookup_playlist_from_name(self, name): """ Helper function to retrieve a playlist from its unique MPD name. """ if not self._playlist_uri_from_name: self.refresh_playlists_mapping() if name not in self._playlist_uri_from_name: return None uri = self._playlist_uri_from_name[name] return self.core.playlists.lookup(uri).get()
[docs] def lookup_playlist_name_from_uri(self, uri): """ Helper function to retrieve the unique MPD playlist name from its uri. """ if uri not in self._playlist_name_from_uri: self.refresh_playlists_mapping() return self._playlist_name_from_uri[uri]
# TODO: consider making context.browse(path) which uses this internally. # advantage would be that all browse requests then go through the same code # and we could prebuild/cache path->uri relationships instead of having to # look them up all the time. def directory_path_to_uri(self, path): parts = re.findall(r'[^/]+', path) uri = None for part in parts: for ref in self.core.library.browse(uri).get(): if ref.type == ref.DIRECTORY and ref.name == part: uri = ref.uri break else: raise exceptions.MpdNoExistError() return uri