diff --git a/docs/api/mpd.rst b/docs/api/mpd.rst index 7bf7fe7b..6361e909 100644 --- a/docs/api/mpd.rst +++ b/docs/api/mpd.rst @@ -4,6 +4,8 @@ .. automodule:: mopidy.frontends.mpd :synopsis: MPD frontend + :members: + :undoc-members: MPD server @@ -17,10 +19,21 @@ 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 :members: :undoc-members: diff --git a/docs/changes.rst b/docs/changes.rst index 341ef850..4fdc8c1f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -39,8 +39,6 @@ greatly improved MPD client support. the packages created by ``setup.py`` for i.e. PyPI. - MPD frontend: - - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. - - Split gigantic protocol implementation into eleven modules. - Search improvements, including support for multi-word search. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty or when a current track is set. @@ -56,8 +54,14 @@ greatly improved MPD client support. - Fix ``load`` so that one can append a playlist to the current playlist, and make it return the correct error message if the playlist is not found. - Support for single track repeat added. (Fixes: :issue:`4`) + - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. + - Split gigantic protocol implementation into eleven modules. - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. + - Remove setting :attr:`mopidy.settings.SERVER` and + :attr:`mopidy.settings.FRONTEND` in favour of the new + :attr:`mopidy.settings.FRONTENDS`. + - Run MPD server in its own process. - Backends: diff --git a/docs/conf.py b/docs/conf.py index c95c39df..d0d8f3af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -130,7 +130,7 @@ html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. diff --git a/docs/development/internals.rst b/docs/development/internals.rst index 085b55ac..0af13aa8 100644 --- a/docs/development/internals.rst +++ b/docs/development/internals.rst @@ -12,40 +12,94 @@ In addition to what you'll find here, don't forget the :doc:`/api/index`. Class instantiation and usage ============================= -The following diagram shows how Mopidy with the despotify backend and ALSA -mixer is wired together. The gray nodes are part of external dependencies, and -not Mopidy. The red nodes lives in the ``main`` process (running an -:mod:`asyncore` loop), while the blue nodes lives in a secondary process named -``core`` (running a service loop in :class:`mopidy.core.CoreProcess`). +The following diagram shows how Mopidy is wired together with the MPD client, +the Spotify service, and the speakers. + +**Legend** + +- Filled red boxes are the key external systems. +- Gray boxes are external dependencies. +- Blue circles lives in the ``main`` process, also known as ``CoreProcess``. + It processing messages on the core queue. +- Purple circles lives in a process named ``MpdProcess``, running an + :mod:`asyncore` loop. +- Green circles lives in a process named ``GStreamerProcess``. +- Brown circle is a thread living in the ``CoreProcess``. .. digraph:: class_instantiation_and_usage - "spytify" [ color="gray" ] - "despotify" [ color="gray" ] - "alsaaudio" [ color="gray" ] - "__main__" [ color="red" ] + "main" [ color="blue" ] "CoreProcess" [ color="blue" ] - "DespotifyBackend" [ color="blue" ] - "AlsaMixer" [ color="blue" ] + + # Frontend + "MPD client" [ color="red", style="filled", shape="box" ] "MpdFrontend" [ color="blue" ] - "MpdServer" [ color="red" ] - "MpdSession" [ color="red" ] - "__main__" -> "CoreProcess" [ label="create" ] - "__main__" -> "MpdServer" [ label="create" ] - "CoreProcess" -> "DespotifyBackend" [ label="create" ] + "MpdProcess" [ color="purple" ] + "MpdServer" [ color="purple" ] + "MpdSession" [ color="purple" ] + "MpdDispatcher" [ color="blue" ] + + # Backend + "Libspotify\nBackend" [ color="blue" ] + "Libspotify\nSessionManager" [ color="brown" ] + "pyspotify" [ color="gray", shape="box" ] + "libspotify" [ color="gray", shape="box" ] + "Spotify" [ color="red", style="filled", shape="box" ] + + # Output/mixer + "GStreamer\nOutput" [ color="blue" ] + "GStreamer\nSoftwareMixer" [ color="blue" ] + "GStreamer\nProcess" [ color="green" ] + "GStreamer" [ color="gray", shape="box" ] + "Speakers" [ color="red", style="filled", shape="box" ] + + "main" -> "CoreProcess" [ label="create" ] + + # Frontend "CoreProcess" -> "MpdFrontend" [ label="create" ] - "MpdServer" -> "MpdSession" [ label="create one per client" ] - "MpdSession" -> "MpdFrontend" [ label="pass MPD requests to" ] - "MpdFrontend" -> "DespotifyBackend" [ label="use backend API" ] - "DespotifyBackend" -> "AlsaMixer" [ label="create and use mixer API" ] - "DespotifyBackend" -> "spytify" [ label="use Python wrapper" ] - "spytify" -> "despotify" [ label="use C library" ] - "AlsaMixer" -> "alsaaudio" [ label="use Python library" ] + "MpdFrontend" -> "MpdProcess" [ label="create" ] + "MpdFrontend" -> "MpdDispatcher" [ label="create" ] + "MpdProcess" -> "MpdServer" [ label="create" ] + "MpdServer" -> "MpdSession" [ label="create one\nper client" ] + "MpdSession" -> "MpdDispatcher" [ + label="pass requests\nvia core_queue" ] + "MpdDispatcher" -> "MpdSession" [ + label="pass response\nvia reply_to pipe" ] + "MpdDispatcher" -> "Libspotify\nBackend" [ label="use backend API" ] + "MPD client" -> "MpdServer" [ label="connect" ] + "MPD client" -> "MpdSession" [ label="request" ] + "MpdSession" -> "MPD client" [ label="response" ] + + # Backend + "CoreProcess" -> "Libspotify\nBackend" [ label="create" ] + "Libspotify\nBackend" -> "Libspotify\nSessionManager" [ + label="creates and uses" ] + "Libspotify\nSessionManager" -> "Libspotify\nBackend" [ + label="pass commands\nvia core_queue" ] + "Libspotify\nSessionManager" -> "pyspotify" [ label="use Python\nwrapper" ] + "pyspotify" -> "Libspotify\nSessionManager" [ label="use callbacks" ] + "pyspotify" -> "libspotify" [ label="use C library" ] + "libspotify" -> "Spotify" [ label="use service" ] + "Libspotify\nSessionManager" -> "GStreamer\nProcess" [ + label="pass commands\nand audio data\nvia output_queue" ] + + # Output/mixer + "Libspotify\nBackend" -> "GStreamer\nSoftwareMixer" [ + label="create and\nuse mixer API" ] + "GStreamer\nSoftwareMixer" -> "GStreamer\nProcess" [ + label="pass commands\nvia output_queue" ] + "CoreProcess" -> "GStreamer\nOutput" [ label="create" ] + "GStreamer\nOutput" -> "GStreamer\nProcess" [ label="create" ] + "GStreamer\nProcess" -> "GStreamer" [ label="use library" ] + "GStreamer" -> "Speakers" [ label="play audio" ] Thread/process communication ============================ +.. warning:: + This section is currently outdated. + - Everything starts with ``Main``. - ``Main`` creates a ``Core`` process which runs the frontend, backend, and mixer. diff --git a/mopidy/__init__.py b/mopidy/__init__.py index e3321041..7cdcad6a 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -5,9 +5,6 @@ if not (2, 6) <= sys.version_info < (3,): def get_version(): return u'0.1.0a4' -def get_mpd_protocol_version(): - return u'0.16.0' - class MopidyException(Exception): def __init__(self, message, *args, **kwargs): super(MopidyException, self).__init__(message, *args, **kwargs) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index a2230180..20e78f5a 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -1,85 +1,17 @@ -import asyncore -import logging -import logging.handlers -import multiprocessing -import optparse import os import sys +# Add ../ to the path so we can run Mopidy from a Git checkout without +# installing it on the system. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy import get_version, settings, SettingsError -from mopidy.process import CoreProcess -from mopidy.utils import get_class -from mopidy.utils.path import get_or_create_folder -from mopidy.utils.settings import list_settings_optparse_callback - -logger = logging.getLogger('mopidy.main') +from mopidy.core import CoreProcess def main(): - options = _parse_options() - _setup_logging(options.verbosity_level, options.dump) - settings.validate() - logger.info('-- Starting Mopidy --') - get_or_create_folder('~/.mopidy/') - core_queue = multiprocessing.Queue() - get_class(settings.SERVER)(core_queue).start() - output_class = get_class(settings.OUTPUT) - backend_class = get_class(settings.BACKENDS[0]) - frontend_class = get_class(settings.FRONTEND) - core = CoreProcess(core_queue, output_class, backend_class, frontend_class) - core.start() - asyncore.loop() - -def _parse_options(): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) - parser.add_option('-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_option('-v', '--verbose', - action='store_const', const=2, dest='verbosity_level', - help='more output (debug level)') - parser.add_option('--dump', - action='store_true', dest='dump', - help='dump debug log to file') - parser.add_option('--list-settings', - action='callback', callback=list_settings_optparse_callback, - help='list current settings') - return parser.parse_args()[0] - -def _setup_logging(verbosity_level, dump): - _setup_console_logging(verbosity_level) - if dump: - _setup_dump_logging() - -def _setup_console_logging(verbosity_level): - if verbosity_level == 0: - level = logging.WARNING - elif verbosity_level == 2: - level = logging.DEBUG - else: - level = logging.INFO - logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level) - -def _setup_dump_logging(): - root = logging.getLogger('') - root.setLevel(logging.DEBUG) - formatter = logging.Formatter(settings.DUMP_LOG_FORMAT) - handler = logging.handlers.RotatingFileHandler( - settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3) - handler.setFormatter(formatter) - root.addHandler(handler) + # Explictly call run() instead of start(), since we don't need to start + # another process. + CoreProcess().run() if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - sys.exit(0) - except SettingsError, e: - logger.error(e) - sys.exit(1) - except SystemExit, e: - logger.error(e) - sys.exit(1) + main() diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py index 1195e9bc..ed5ba697 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/libspotify/playback.py @@ -4,7 +4,7 @@ import multiprocessing from spotify import Link, SpotifyError from mopidy.backends.base import BasePlaybackController -from mopidy.process import pickle_connection +from mopidy.utils.process import pickle_connection logger = logging.getLogger('mopidy.backends.libspotify.playback') diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 707423aa..22cbb0a0 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -18,7 +18,10 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def __init__(self, username, password, core_queue, output_queue): SpotifySessionManager.__init__(self, username, password) - threading.Thread.__init__(self) + threading.Thread.__init__(self, name='LibspotifySessionManagerThread') + # Run as a daemon thread, so Mopidy won't wait for this thread to exit + # before Mopidy exits. + self.daemon = True self.core_queue = core_queue self.output_queue = output_queue self.connected = threading.Event() diff --git a/mopidy/core.py b/mopidy/core.py new file mode 100644 index 00000000..d823bfa5 --- /dev/null +++ b/mopidy/core.py @@ -0,0 +1,88 @@ +import logging +import multiprocessing +import optparse + +from mopidy import get_version, settings +from mopidy.utils import get_class +from mopidy.utils.log import setup_logging +from mopidy.utils.path import get_or_create_folder +from mopidy.utils.process import BaseProcess, unpickle_connection +from mopidy.utils.settings import list_settings_optparse_callback + +logger = logging.getLogger('mopidy.core') + +class CoreProcess(BaseProcess): + def __init__(self): + super(CoreProcess, self).__init__(name='CoreProcess') + self.core_queue = multiprocessing.Queue() + self.options = self.parse_options() + self.output_queue = None + self.backend = None + self.frontend = None + + def parse_options(self): + parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser.add_option('-q', '--quiet', + action='store_const', const=0, dest='verbosity_level', + help='less output (warning level)') + parser.add_option('-v', '--verbose', + action='store_const', const=2, dest='verbosity_level', + help='more output (debug level)') + parser.add_option('--dump', + action='store_true', dest='dump', + help='dump debug log to file') + parser.add_option('--list-settings', + action='callback', callback=list_settings_optparse_callback, + help='list current settings') + return parser.parse_args()[0] + + def run_inside_try(self): + logger.info(u'-- Starting Mopidy --') + self.setup() + while True: + message = self.core_queue.get() + self.process_message(message) + + def setup(self): + self.setup_logging() + self.setup_settings() + self.output_queue = self.setup_output(self.core_queue) + self.backend = self.setup_backend(self.core_queue, self.output_queue) + self.frontend = self.setup_frontend(self.core_queue, self.backend) + + def setup_logging(self): + setup_logging(self.options.verbosity_level, self.options.dump) + + def setup_settings(self): + get_or_create_folder('~/.mopidy/') + settings.validate() + + def setup_output(self, core_queue): + output_queue = multiprocessing.Queue() + get_class(settings.OUTPUT)(core_queue, output_queue) + return output_queue + + def setup_backend(self, core_queue, output_queue): + return get_class(settings.BACKENDS[0])(core_queue, output_queue) + + def setup_frontend(self, core_queue, backend): + frontend = get_class(settings.FRONTENDS[0])() + frontend.start_server(core_queue) + frontend.create_dispatcher(backend) + return frontend + + def process_message(self, message): + if message.get('to') == 'output': + self.output_queue.put(message) + elif message['command'] == 'mpd_request': + response = self.frontend.dispatcher.handle_request(message['request']) + connection = unpickle_connection(message['reply_to']) + connection.send(response) + elif message['command'] == 'end_of_track': + self.backend.playback.on_end_of_track() + elif message['command'] == 'stop_playback': + self.backend.playback.stop() + elif message['command'] == 'set_stored_playlists': + self.backend.stored_playlists.playlists = message['playlists'] + else: + logger.warning(u'Cannot handle message: %s', message) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 83d6ce4c..02e3ab5f 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,93 +1,32 @@ -import re +from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.frontends.mpd.process import MpdProcess -from mopidy import MopidyException - -class MpdAckError(MopidyException): +class MpdFrontend(object): """ - 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 + The MPD frontend. """ - 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 __init__(self): + self.process = None + self.dispatcher = None - 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.error_code, self.index, self.command, self.message) + self.process = MpdProcess(core_queue) + self.process.start() -class MpdArgError(MpdAckError): - def __init__(self, *args, **kwargs): - super(MpdArgError, self).__init__(*args, **kwargs) - self.error_code = 2 # ACK_ERROR_ARG + def create_dispatcher(self, backend): + """ + Creates a dispatcher for MPD requests. -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' - -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.+)$') - 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 + :param backend: the backend + :type backend: :class:`mopidy.backends.base.BaseBackend` + :rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher` + """ + self.dispatcher = MpdDispatcher(backend) + return self.dispatcher diff --git a/mopidy/frontends/mpd/frontend.py b/mopidy/frontends/mpd/dispatcher.py similarity index 82% rename from mopidy/frontends/mpd/frontend.py rename to mopidy/frontends/mpd/dispatcher.py index 9a0251eb..2a477e1c 100644 --- a/mopidy/frontends/mpd/frontend.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -1,20 +1,18 @@ -import logging import re -from mopidy.frontends.mpd import (mpd_commands, request_handlers, - handle_pattern, MpdAckError, MpdArgError, MpdUnknownCommand) +from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, + MpdUnknownCommand) +from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers # Do not remove the following import. The protocol modules must be imported to # get them registered as request handlers. from mopidy.frontends.mpd.protocol import (audio_output, command_list, - connection, current_playlist, music_db, playback, reflection, status, - stickers, stored_playlists) + connection, current_playlist, empty, music_db, playback, reflection, + status, stickers, stored_playlists) from mopidy.utils import flatten -logger = logging.getLogger('mopidy.frontends.mpd.frontend') - -class MpdFrontend(object): +class MpdDispatcher(object): """ - The MPD frontend dispatches MPD requests to the correct handler. + Dispatches MPD requests to the correct handler. """ 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')): response.append(u'OK') return response - -@handle_pattern(r'^$') -def empty(frontend): - """The original MPD server returns ``OK`` on an empty request.""" - pass diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py new file mode 100644 index 00000000..2a18b2f3 --- /dev/null +++ b/mopidy/frontends/mpd/exceptions.py @@ -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' diff --git a/mopidy/frontends/mpd/process.py b/mopidy/frontends/mpd/process.py new file mode 100644 index 00000000..7bd95900 --- /dev/null +++ b/mopidy/frontends/mpd/process.py @@ -0,0 +1,18 @@ +import asyncore +import logging + +from mopidy.frontends.mpd.server import MpdServer +from mopidy.utils.process import BaseProcess + +logger = logging.getLogger('mopidy.frontends.mpd.process') + +class MpdProcess(BaseProcess): + def __init__(self, core_queue): + super(MpdProcess, self).__init__(name='MpdProcess') + self.core_queue = core_queue + + def run_inside_try(self): + logger.debug(u'Starting MPD server process') + server = MpdServer(self.core_queue) + server.start() + asyncore.loop() diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 00932e90..756aa3c3 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -10,8 +10,47 @@ implement our own MPD server which is compatible with the numerous existing `MPD clients `_. """ +import re + #: The MPD protocol uses UTF-8 for encoding all data. ENCODING = u'utf-8' #: The MPD protocol uses ``\n`` as line terminator. 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.+)$') + 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 diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py index e659b162..d25fc118 100644 --- a/mopidy/frontends/mpd/protocol/audio_output.py +++ b/mopidy/frontends/mpd/protocol/audio_output.py @@ -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\d+)"$') def disableoutput(frontend, outputid): diff --git a/mopidy/frontends/mpd/protocol/command_list.py b/mopidy/frontends/mpd/protocol/command_list.py index 900c26b0..b3df0be6 100644 --- a/mopidy/frontends/mpd/protocol/command_list.py +++ b/mopidy/frontends/mpd/protocol/command_list.py @@ -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$') def command_list_begin(frontend): diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py index 4312ded5..0ce3ef51 100644 --- a/mopidy/frontends/mpd/protocol/connection.py +++ b/mopidy/frontends/mpd/protocol/connection.py @@ -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$') def close(frontend): @@ -9,8 +10,7 @@ def close(frontend): Closes the connection to MPD. """ - # TODO Does not work after multiprocessing branch merge - #frontend.session.do_close() + pass # TODO @handle_pattern(r'^kill$') def kill(frontend): @@ -21,8 +21,7 @@ def kill(frontend): Kills MPD. """ - # TODO Does not work after multiprocessing branch merge - #frontend.session.do_kill() + pass # TODO @handle_pattern(r'^password "(?P[^"]+)"$') def password_(frontend, password): diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index b9111d9e..90a53f5f 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -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) @handle_pattern(r'^add "(?P[^"]*)"$') diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py new file mode 100644 index 00000000..a39d79eb --- /dev/null +++ b/mopidy/frontends/mpd/protocol/empty.py @@ -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 diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 5aec6eae..d4dcf50d 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -1,7 +1,7 @@ import re -from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented -from mopidy.frontends.mpd.protocol import stored_playlists +from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists +from mopidy.frontends.mpd.exceptions import MpdNotImplemented def _build_query(mpd_query): """ diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 4c602f3b..c3fbdd5f 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -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) @handle_pattern(r'^consume (?P[01])$') diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 0c349746..d2c9c599 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -1,5 +1,5 @@ -from mopidy.frontends.mpd import (handle_pattern, mpd_commands, - MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands +from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_pattern(r'^commands$') def commands(frontend): diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 16e73dea..e18f1ea4 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -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$') def clearerror(frontend): diff --git a/mopidy/frontends/mpd/protocol/stickers.py b/mopidy/frontends/mpd/protocol/stickers.py index c184d1f9..145665eb 100644 --- a/mopidy/frontends/mpd/protocol/stickers.py +++ b/mopidy/frontends/mpd/protocol/stickers.py @@ -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[^"]+)" ' r'"(?P[^"]+)"( "(?P[^"]+)")*$') diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index 3d7a8710..c34b1676 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -1,7 +1,7 @@ import datetime as dt -from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError, - MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented @handle_pattern(r'^listplaylist "(?P[^"]+)"$') def listplaylist(frontend, name): diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 91d8e67a..db13e516 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -1,21 +1,18 @@ -import asynchat import asyncore import logging -import multiprocessing import re import socket import sys -from mopidy import get_mpd_protocol_version, settings -from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR -from mopidy.process import pickle_connection -from mopidy.utils import indent +from mopidy import settings +from .session import MpdSession logger = logging.getLogger('mopidy.frontends.mpd.server') 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): @@ -58,65 +55,3 @@ class MpdServer(asyncore.dispatcher): and re.match('\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % 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) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py new file mode 100644 index 00000000..72a1f845 --- /dev/null +++ b/mopidy/frontends/mpd/session.py @@ -0,0 +1,70 @@ +import asynchat +import logging +import multiprocessing + +from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION +from mopidy.utils.log import indent +from mopidy.utils.process import pickle_connection + +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) diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 2910ef72..1225cafd 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,7 +1,7 @@ import multiprocessing from mopidy.mixers import BaseMixer -from mopidy.process import pickle_connection +from mopidy.utils.process import pickle_connection class GStreamerSoftwareMixer(BaseMixer): """Mixer which uses GStreamer to control volume in software.""" diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index d78863aa..f859791b 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -3,9 +3,9 @@ from serial import Serial from multiprocessing import Pipe from mopidy.mixers import BaseMixer -from mopidy.process import BaseProcess from mopidy.settings import (MIXER_EXT_PORT, MIXER_EXT_SOURCE, MIXER_EXT_SPEAKERS_A, MIXER_EXT_SPEAKERS_B) +from mopidy.utils.process import BaseProcess logger = logging.getLogger('mopidy.mixers.nad') @@ -74,7 +74,7 @@ class NadTalker(BaseProcess): _nad_volume = None def __init__(self, pipe=None): - super(NadTalker, self).__init__() + super(NadTalker, self).__init__(name='NadTalker') self.pipe = pipe self._device = None diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 8f321976..667b815b 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -9,7 +9,7 @@ import logging import threading from mopidy import settings -from mopidy.process import BaseProcess, unpickle_connection +from mopidy.utils.process import BaseProcess, unpickle_connection logger = logging.getLogger('mopidy.outputs.gstreamer') @@ -44,7 +44,7 @@ class GStreamerProcess(BaseProcess): """ def __init__(self, core_queue, output_queue): - super(GStreamerProcess, self).__init__() + super(GStreamerProcess, self).__init__(name='GStreamerProcess') self.core_queue = core_queue self.output_queue = output_queue self.gst_pipeline = None diff --git a/mopidy/process.py b/mopidy/process.py deleted file mode 100644 index 5ad1b0b4..00000000 --- a/mopidy/process.py +++ /dev/null @@ -1,80 +0,0 @@ -import logging -import multiprocessing -from multiprocessing.reduction import reduce_connection -import pickle -import sys - -from mopidy import SettingsError - -logger = logging.getLogger('mopidy.process') - -def pickle_connection(connection): - return pickle.dumps(reduce_connection(connection)) - -def unpickle_connection(pickled_connection): - # From http://stackoverflow.com/questions/1446004 - (func, args) = pickle.loads(pickled_connection) - return func(*args) - - -class BaseProcess(multiprocessing.Process): - def run(self): - try: - self.run_inside_try() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - sys.exit(0) - except SettingsError as e: - logger.error(e.message) - sys.exit(1) - except ImportError as e: - logger.error(e) - sys.exit(1) - except Exception as e: - logger.exception(e) - raise e - - def run_inside_try(self): - raise NotImplementedError - - -class CoreProcess(BaseProcess): - def __init__(self, core_queue, output_class, backend_class, - frontend_class): - super(CoreProcess, self).__init__() - self.core_queue = core_queue - self.output_queue = None - self.output_class = output_class - self.backend_class = backend_class - self.frontend_class = frontend_class - self.output = None - self.backend = None - self.frontend = None - - def run_inside_try(self): - self.setup() - while True: - message = self.core_queue.get() - self.process_message(message) - - def setup(self): - self.output_queue = multiprocessing.Queue() - self.output = self.output_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) - - def process_message(self, message): - if message.get('to') == 'output': - self.output_queue.put(message) - elif message['command'] == 'mpd_request': - response = self.frontend.handle_request(message['request']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'end_of_track': - self.backend.playback.on_end_of_track() - elif message['command'] == 'stop_playback': - self.backend.playback.stop() - elif message['command'] == 'set_stored_playlists': - self.backend.stored_playlists.playlists = message['playlists'] - else: - logger.warning(u'Cannot handle message: %s', message) diff --git a/mopidy/settings.py b/mopidy/settings.py index 3198ed93..4268f059 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -41,12 +41,15 @@ DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT #: DUMP_LOG_FILENAME = u'dump.log' DUMP_LOG_FILENAME = u'dump.log' -#: Protocol frontend to use. +#: List of server frontends to use. #: #: Default:: #: -#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' -FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' +#: FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',) +#: +#: .. note:: +#: Currently only the first frontend in the list is used. +FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',) #: 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' -#: 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. #: #:Examples: diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index 277d2f3b..acbb4664 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -27,12 +27,3 @@ def get_class(name): except (ImportError, AttributeError): raise ImportError("Couldn't load: %s" % name) return class_object - -def indent(string, places=4, linebreak='\n'): - lines = string.split(linebreak) - if len(lines) == 1: - return string - result = u'' - for line in lines: - result += linebreak + ' ' * places + line - return result diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py new file mode 100644 index 00000000..c892102a --- /dev/null +++ b/mopidy/utils/log.py @@ -0,0 +1,36 @@ +import logging +import logging.handlers + +from mopidy import settings + +def setup_logging(verbosity_level, dump): + setup_console_logging(verbosity_level) + if dump: + setup_dump_logging() + +def setup_console_logging(verbosity_level): + if verbosity_level == 0: + level = logging.WARNING + elif verbosity_level == 2: + level = logging.DEBUG + else: + level = logging.INFO + logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level) + +def setup_dump_logging(): + root = logging.getLogger('') + root.setLevel(logging.DEBUG) + formatter = logging.Formatter(settings.DUMP_LOG_FORMAT) + handler = logging.handlers.RotatingFileHandler( + settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3) + handler.setFormatter(formatter) + root.addHandler(handler) + +def indent(string, places=4, linebreak='\n'): + lines = string.split(linebreak) + if len(lines) == 1: + return string + result = u'' + for line in lines: + result += linebreak + ' ' * places + line + return result diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py new file mode 100644 index 00000000..73224840 --- /dev/null +++ b/mopidy/utils/process.py @@ -0,0 +1,39 @@ +import logging +import multiprocessing +from multiprocessing.reduction import reduce_connection +import pickle +import sys + +from mopidy import SettingsError + +logger = logging.getLogger('mopidy.utils.process') + +def pickle_connection(connection): + return pickle.dumps(reduce_connection(connection)) + +def unpickle_connection(pickled_connection): + # From http://stackoverflow.com/questions/1446004 + (func, args) = pickle.loads(pickled_connection) + return func(*args) + + +class BaseProcess(multiprocessing.Process): + def run(self): + logger.debug(u'%s: Starting process', self.name) + try: + self.run_inside_try() + except KeyboardInterrupt: + logger.info(u'%s: Interrupted by user', self.name) + sys.exit(0) + except SettingsError as e: + logger.error(e.message) + sys.exit(1) + except ImportError as e: + logger.error(e) + sys.exit(1) + except Exception as e: + logger.exception(e) + raise e + + def run_inside_try(self): + raise NotImplementedError diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 478a03e6..e45c5521 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -6,7 +6,7 @@ import os import sys from mopidy import SettingsError -from mopidy.utils import indent +from mopidy.utils.log import indent logger = logging.getLogger('mopidy.utils.settings') @@ -37,7 +37,7 @@ class SettingsProxy(object): def current(self): current = copy(self.default) current.update(self.local) - return current + return current def __getattr__(self, attr): if not self._is_setting(attr): @@ -81,6 +81,8 @@ def validate_settings(defaults, settings): errors = {} changed = { + 'FRONTEND': 'FRONTENDS', + 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT', 'SPOTIFY_LIB_APPKEY': None, diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index 24201341..b81e727e 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py index 683a1013..6c801c3f 100644 --- a/tests/frontends/mpd/command_list_test.py +++ b/tests/frontends/mpd/command_list_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class CommandListsTest(unittest.TestCase): def setUp(self): 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): result = self.h.handle_request(u'command_list_begin') diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 341e630c..21753054 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_close(self): result = self.h.handle_request(u'close') diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index e27e58c5..c53e2b8d 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -1,14 +1,14 @@ import unittest 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.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_add(self): needle = Track(uri='dummy://foo') diff --git a/tests/frontends/mpd/request_handler_test.py b/tests/frontends/mpd/dispatcher_test.py similarity index 75% rename from tests/frontends/mpd/request_handler_test.py rename to tests/frontends/mpd/dispatcher_test.py index ac8bd7e9..2a2ee4db 100644 --- a/tests/frontends/mpd/request_handler_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -1,19 +1,21 @@ import unittest 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 -class RequestHandlerTest(unittest.TestCase): +class MpdDispatcherTest(unittest.TestCase): def setUp(self): 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): func = lambda: None try: - frontend.handle_pattern('a pattern')(func) - frontend.handle_pattern('a pattern')(func) + handle_pattern('a pattern')(func) + handle_pattern('a pattern')(func) self.fail('Registering a pattern twice shoulde raise ValueError') except ValueError: pass @@ -28,7 +30,7 @@ class RequestHandlerTest(unittest.TestCase): def test_finding_handler_for_known_command_returns_handler_and_kwargs(self): expected_handler = lambda x: None - frontend.request_handlers['known_command (?P.+)'] = \ + request_handlers['known_command (?P.+)'] = \ expected_handler (handler, kwargs) = self.h.find_handler('known_command an_arg') self.assertEqual(handler, expected_handler) @@ -41,7 +43,7 @@ class RequestHandlerTest(unittest.TestCase): def test_handling_known_request(self): expected = 'magic' - frontend.request_handlers['known request'] = lambda x: expected + request_handlers['known request'] = lambda x: expected result = self.h.handle_request('known request') self.assert_(u'OK' in result) self.assert_(expected in result) diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py index e337550f..ef222d46 100644 --- a/tests/frontends/mpd/exception_test.py +++ b/tests/frontends/mpd/exception_test.py @@ -1,6 +1,6 @@ import unittest -from mopidy.frontends.mpd import (MpdAckError, MpdUnknownCommand, +from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand, MpdNotImplemented) class MpdExceptionsTest(unittest.TestCase): diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index fc8f980a..5fcc393c 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_count(self): result = self.h.handle_request(u'count "tag" "needle"') diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 17263aef..3ba48a54 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -1,14 +1,14 @@ import unittest 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.models import Track class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): 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): result = self.h.handle_request(u'consume "0"') @@ -167,7 +167,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_next(self): result = self.h.handle_request(u'next') diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index 5491946c..a4491d75 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class ReflectionHandlerTest(unittest.TestCase): def setUp(self): 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): result = self.h.handle_request(u'commands') diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 9839acfe..fbd0ff9e 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,14 +1,14 @@ import unittest 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.models import Track class StatusHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_clearerror(self): result = self.h.handle_request(u'clearerror') @@ -51,7 +51,7 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_stats_method(self): - result = frontend.status.stats(self.h) + result = dispatcher.status.stats(self.h) self.assert_('artists' in result) self.assert_(int(result['artists']) >= 0) self.assert_('albums' in result) @@ -72,106 +72,106 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) 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.assertEqual(int(result['volume']), 0) def test_status_method_contains_volume(self): 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.assertEqual(int(result['volume']), 17) 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.assertEqual(int(result['repeat']), 0) def test_status_method_contains_repeat_is_1(self): 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.assertEqual(int(result['repeat']), 1) 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.assertEqual(int(result['random']), 0) def test_status_method_contains_random_is_1(self): 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.assertEqual(int(result['random']), 1) 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_(int(result['single']) in (0, 1)) 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.assertEqual(int(result['consume']), 0) def test_status_method_contains_consume_is_1(self): 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.assertEqual(int(result['consume']), 1) 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_(int(result['playlist']) in xrange(0, 2**31 - 1)) 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_(int(result['playlistlength']) >= 0) 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_(int(result['xfade']) >= 0) def test_status_method_contains_state_is_play(self): 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.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): 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.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): self.b.playback.state = self.b.playback.PLAYING 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.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): self.b.current_playlist.append([Track()]) 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_(int(result['song']) >= 0) def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): self.b.current_playlist.append([Track()]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('songid' in result) self.assertEqual(int(result['songid']), 1) def test_status_method_when_playing_contains_time_with_no_length(self): self.b.current_playlist.append([Track(length=None)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('time' in result) (position, total) = result['time'].split(':') position = int(position) @@ -181,7 +181,7 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_time_with_length(self): self.b.current_playlist.append([Track(length=10000)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('time' in result) (position, total) = result['time'].split(':') position = int(position) @@ -191,13 +191,13 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_elapsed(self): self.b.playback.state = self.b.playback.PAUSED 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.assertEqual(int(result['elapsed']), 59123) def test_status_method_when_playing_contains_bitrate(self): self.b.current_playlist.append([Track(bitrate=320)]) self.b.playback.play() - result = dict(frontend.status.status(self.h)) + result = dict(dispatcher.status.status(self.h)) self.assert_('bitrate' in result) self.assertEqual(int(result['bitrate']), 320) diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py index 401eaf57..5b66d723 100644 --- a/tests/frontends/mpd/stickers_test.py +++ b/tests/frontends/mpd/stickers_test.py @@ -1,13 +1,13 @@ import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import frontend +from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer class StickersHandlerTest(unittest.TestCase): def setUp(self): 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): result = self.h.handle_request( diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index 6e5717af..a24cbb88 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -2,14 +2,14 @@ import datetime as dt import unittest 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.models import Track, Playlist class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) - self.h = frontend.MpdFrontend(backend=self.b) + self.h = dispatcher.MpdDispatcher(backend=self.b) def test_listplaylist(self): self.b.stored_playlists.playlists = [ diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index db15c952..52d1fbe1 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -3,8 +3,8 @@ import unittest from mopidy import settings from mopidy.outputs.gstreamer import GStreamerOutput -from mopidy.process import pickle_connection from mopidy.utils.path import path_to_uri +from mopidy.utils.process import pickle_connection from tests import data_folder, SkipTest