Replace the playlist uri/name mapping with a mapping for all the items in browse. Check the mapping when browsing a path. This has the added benefit of serving as a cache for browse, so we don't need to traverse the path parts and lookup each of them for each call to browse.
333 lines
11 KiB
Python
333 lines
11 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import logging
|
|
import re
|
|
|
|
import pykka
|
|
|
|
from mopidy.mpd import exceptions, protocol, tokenize
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
protocol.load_protocol_modules()
|
|
|
|
|
|
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)
|
|
|
|
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 = protocol.commands.handlers.get(command_name)
|
|
if command and not command.auth_required:
|
|
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):
|
|
tokens = tokenize.split(request)
|
|
try:
|
|
return protocol.commands.call(tokens, context=self.context)
|
|
except exceptions.MpdAckError as exc:
|
|
if exc.command is None:
|
|
exc.command = tokens[0]
|
|
raise
|
|
|
|
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]
|
|
|
|
|
|
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_browse_chars = re.compile(r'[\n\r]')
|
|
_invalid_playlist_chars = re.compile(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._uri_from_name = {}
|
|
self._name_from_uri = {}
|
|
self.refresh_playlists_mapping()
|
|
|
|
def create_unique_name(self, name, uri):
|
|
stripped_name = self._invalid_browse_chars.sub(' ', name)
|
|
name = stripped_name
|
|
i = 2
|
|
while name in self._uri_from_name:
|
|
if self._uri_from_name[name] == uri:
|
|
return name
|
|
name = '%s [%d]' % (stripped_name, i)
|
|
i += 1
|
|
return name
|
|
|
|
def insert_name_uri_mapping(self, name, uri):
|
|
name = self.create_unique_name(name, uri)
|
|
self._uri_from_name[name] = uri
|
|
self._name_from_uri[uri] = name
|
|
return name
|
|
|
|
def refresh_playlists_mapping(self):
|
|
"""
|
|
Maintain map between playlists and unique playlist names to be used by
|
|
MPD
|
|
"""
|
|
if self.core is not None:
|
|
for playlist in self.core.playlists.playlists.get():
|
|
if not playlist.name:
|
|
continue
|
|
# TODO: add scheme to name perhaps 'foo (spotify)' etc.
|
|
name = self._invalid_playlist_chars.sub(' ', playlist.name)
|
|
self.insert_name_uri_mapping(name, playlist.uri)
|
|
|
|
def lookup_playlist_from_name(self, name):
|
|
"""
|
|
Helper function to retrieve a playlist from its unique MPD name.
|
|
"""
|
|
if not self._uri_from_name:
|
|
self.refresh_playlists_mapping()
|
|
if name not in self._uri_from_name:
|
|
return None
|
|
uri = self._uri_from_name[name]
|
|
return self.core.playlists.lookup(uri).get()
|
|
|
|
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._name_from_uri:
|
|
self.refresh_playlists_mapping()
|
|
return self._name_from_uri[uri]
|
|
|
|
def browse(self, path, recursive=True, lookup=True):
|
|
path_parts = re.findall(r'[^/]+', path or '')
|
|
root_path = '/'.join([''] + path_parts)
|
|
|
|
if root_path not in self._uri_from_name:
|
|
uri = None
|
|
for part in path_parts:
|
|
for ref in self.core.library.browse(uri).get():
|
|
if (ref.type in (ref.DIRECTORY, ref.ALBUM, ref.PLAYLIST)
|
|
and ref.name == part):
|
|
uri = ref.uri
|
|
break
|
|
else:
|
|
raise exceptions.MpdNoExistError('Not found')
|
|
root_path = self.insert_name_uri_mapping(root_path, uri)
|
|
|
|
else:
|
|
uri = self._uri_from_name[root_path]
|
|
|
|
if recursive:
|
|
yield (root_path, None)
|
|
|
|
path_and_futures = [(root_path, self.core.library.browse(uri))]
|
|
while path_and_futures:
|
|
base_path, future = path_and_futures.pop()
|
|
for ref in future.get():
|
|
path = '/'.join([base_path, ref.name.replace('/', '')])
|
|
path = self.insert_name_uri_mapping(path, ref.uri)
|
|
|
|
if ref.type in (ref.DIRECTORY, ref.ALBUM, ref.PLAYLIST):
|
|
yield (path, None)
|
|
if recursive:
|
|
path_and_futures.append(
|
|
(path, self.core.library.browse(ref.uri)))
|
|
elif ref.type == ref.TRACK:
|
|
if lookup:
|
|
yield (path, self.core.library.lookup(ref.uri))
|
|
else:
|
|
yield (path, ref)
|