From bdba83b1be3371a42ec8cdcefae47d976e72b3a4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 00:09:17 +0100 Subject: [PATCH 01/43] main: Move argparse building to a function. --- mopidy/__main__.py | 4 ++- mopidy/commands.py | 69 ++++++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index aa0c751e..c8af0600 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -30,7 +30,9 @@ def main(): signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) - args = commands.parser.parse_args(args=mopidy_args) + parser = commands.build_parser() + args = parser.parse_args(args=mopidy_args) + if args.show_config: commands.show_config(args) if args.show_deps: diff --git a/mopidy/commands.py b/mopidy/commands.py index 598be043..0f7e6d9b 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -21,39 +21,42 @@ def config_override_type(value): '%s must have the format section/key=value' % value) -parser = argparse.ArgumentParser() -parser.add_argument( - '--version', action='version', - version='Mopidy %s' % versioning.get_version()) -parser.add_argument( - '-q', '--quiet', - action='store_const', const=-1, dest='verbosity_level', - help='less output (warning level)') -parser.add_argument( - '-v', '--verbose', - action='count', dest='verbosity_level', - help='more output (debug level)') -parser.add_argument( - '--save-debug-log', - action='store_true', dest='save_debug_log', - help='save debug log to "./mopidy.log"') -parser.add_argument( - '--show-config', - action='store_true', dest='show_config', - help='show current config') -parser.add_argument( - '--show-deps', - action='store_true', dest='show_deps', - help='show dependencies and their versions') -parser.add_argument( - '--config', - action='store', dest='config_files', type=config_files_type, - default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf', - help='config files to use, colon seperated, later files override') -parser.add_argument( - '-o', '--option', - action='append', dest='config_overrides', type=config_override_type, - help='`section/key=value` values to override config options') +def build_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--version', action='version', + version='Mopidy %s' % versioning.get_version()) + parser.add_argument( + '-q', '--quiet', + action='store_const', const=-1, dest='verbosity_level', + help='less output (warning level)') + parser.add_argument( + '-v', '--verbose', + action='count', dest='verbosity_level', + help='more output (debug level)') + parser.add_argument( + '--save-debug-log', + action='store_true', dest='save_debug_log', + help='save debug log to "./mopidy.log"') + parser.add_argument( + '--show-config', + action='store_true', dest='show_config', + help='show current config') + parser.add_argument( + '--show-deps', + action='store_true', dest='show_deps', + help='show dependencies and their versions') + parser.add_argument( + '--config', + action='store', dest='config_files', type=config_files_type, + default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf', + help='config files to use, colon seperated, later files override') + parser.add_argument( + '-o', '--option', + action='append', dest='config_overrides', type=config_override_type, + help='`section/key=value` values to override config options') + + return parser def show_config(args): From 51b778fcd655a9cd6b2f53ec228e7dd6a352ca50 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 00:59:05 +0100 Subject: [PATCH 02/43] main: Create helper for logging boostraping --- mopidy/__main__.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c8af0600..539d6576 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -38,20 +38,9 @@ def main(): if args.show_deps: commands.show_deps() - # TODO: figure out a way to make the boilerplate in this file reusable in - # scanner and other places we need it. + bootstrap_logging(args) try: - # Initial config without extensions to bootstrap logging. - logging_initialized = False - logging_config, _ = config_lib.load( - args.config_files, [], args.config_overrides) - - # TODO: setup_logging needs defaults in-case config values are None - log.setup_logging( - logging_config, args.verbosity_level, args.save_debug_log) - logging_initialized = True - create_file_structures() check_old_locations() @@ -84,11 +73,20 @@ def main(): except KeyboardInterrupt: pass except Exception as ex: - if logging_initialized: - logger.exception(ex) + logger.exception(ex) raise +def bootstrap_logging(args): + # Initial config without extensions to bootstrap logging. + logging_config, _ = config_lib.load( + args.config_files, [], args.config_overrides) + + # TODO: setup_logging needs defaults in-case config values are None + log.setup_logging( + logging_config, args.verbosity_level, args.save_debug_log) + + def create_file_structures(): path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') From f49973304cd8fe2ac43e6906da6b291d4328e41a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:12:45 +0100 Subject: [PATCH 03/43] main: Start unifying command handling - Removes show_deps and show_config from commands module. These are now handled directly in the main() method pending subcommands. - Unifies show_config with general main() config handling. - Sets default verbosity level to zero. - Reduce verbosity when --show-config or --show-deps is called. - Update console logging to consider verbosity < 0 quiet/ --- mopidy/__main__.py | 40 +++++++++++++++++++++++++++++----------- mopidy/commands.py | 32 ++------------------------------ mopidy/utils/log.py | 2 +- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 539d6576..af83dcd8 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -21,7 +21,7 @@ from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib from mopidy.core import Core -from mopidy.utils import log, path, process +from mopidy.utils import deps, log, path, process logger = logging.getLogger('mopidy.main') @@ -33,10 +33,8 @@ def main(): parser = commands.build_parser() args = parser.parse_args(args=mopidy_args) - if args.show_config: - commands.show_config(args) - if args.show_deps: - commands.show_deps() + if args.show_deps or args.show_config: + args.verbosity_level -= 1 bootstrap_logging(args) @@ -45,20 +43,40 @@ def main(): check_old_locations() installed_extensions = ext.load_extensions() - config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) - # Filter out disabled extensions and remove any config errors for them. enabled_extensions = [] for extension in installed_extensions: - enabled = config[extension.ext_name]['enabled'] - if ext.validate_extension(extension) and enabled: + if not ext.validate_extension(extension): + config[extension.ext_name] = {b'enabled': False} + config_errors[extension.ext_name] = { + b'enabled': b'extension disabled by self check.'} + elif not config[extension.ext_name]['enabled']: + config[extension.ext_name] = {b'enabled': False} + config_errors[extension.ext_name] = { + b'enabled': b'extension disabled by user config.'} + else: enabled_extensions.append(extension) - elif extension.ext_name in config_errors: - del config_errors[extension.ext_name] log_extension_info(installed_extensions, enabled_extensions) + + # TODO: move to 'mopidy config' and 'mopidy deps' + if args.show_config: + logger.info('Dumping sanitized user config and exiting.') + print config_lib.format( + config, installed_extensions, config_errors) + sys.exit(0) + if args.show_deps: + logger.info('Dumping debug info about dependencies and exiting.') + print deps.format_dependency_list() + sys.exit(0) + + # Remove errors for extensions that are not enabled: + for extension in installed_extensions: + if extension not in enabled_extensions: + config_errors.pop(extension.ext_name, None) + check_config_errors(config_errors) # Read-only config from here on, please. diff --git a/mopidy/commands.py b/mopidy/commands.py index 0f7e6d9b..36269dca 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals import argparse -import sys -from mopidy import config as config_lib, ext -from mopidy.utils import deps, versioning +from mopidy.utils import versioning def config_files_type(value): @@ -23,6 +21,7 @@ def config_override_type(value): def build_parser(): parser = argparse.ArgumentParser() + parser.set_defaults(verbosity_level=0) parser.add_argument( '--version', action='version', version='Mopidy %s' % versioning.get_version()) @@ -57,30 +56,3 @@ def build_parser(): help='`section/key=value` values to override config options') return parser - - -def show_config(args): - """Prints the effective config and exits.""" - extensions = ext.load_extensions() - config, errors = config_lib.load( - args.config_files, extensions, args.config_overrides) - - # Clear out any config for disabled extensions. - for extension in extensions: - if not ext.validate_extension(extension): - config[extension.ext_name] = {b'enabled': False} - errors[extension.ext_name] = { - b'enabled': b'extension disabled itself.'} - elif not config[extension.ext_name]['enabled']: - config[extension.ext_name] = {b'enabled': False} - errors[extension.ext_name] = { - b'enabled': b'extension disabled by config.'} - - print config_lib.format(config, extensions, errors) - sys.exit(0) - - -def show_deps(): - """Prints a list of all dependencies and exits.""" - print deps.format_dependency_list() - sys.exit(0) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 715aca1a..9f3c973d 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -34,7 +34,7 @@ def setup_root_logger(): def setup_console_logging(config, verbosity_level): - if verbosity_level == -1: + if verbosity_level < 0: log_level = logging.WARNING log_format = config['logging']['console_format'] elif verbosity_level >= 1: From 9539c2ac35635bb59511ce8f73c376cfd7702836 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:27:28 +0100 Subject: [PATCH 04/43] main: Switch to subcommands - show-deps replaced with 'mopidy deps' - show-config replaced with 'mopidy config' - Just running mopidy now displays help, run 'mopidy run' to start server. --- mopidy/__main__.py | 16 ++++++++++++---- mopidy/commands.py | 8 -------- tests/help_test.py | 1 - 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index af83dcd8..bc5ca86c 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -31,9 +31,18 @@ def main(): signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) parser = commands.build_parser() + subparser = parser.add_subparsers(title='commands', metavar='COMMAND') + + run_parser = subparser.add_parser('run', help='start mopidy server') + run_parser.set_defaults(command='run') + config_parser = subparser.add_parser('config', help='show current config') + config_parser.set_defaults(command='config') + deps_parser = subparser.add_parser('deps', help='show dependencies') + deps_parser.set_defaults(command='deps') + args = parser.parse_args(args=mopidy_args) - if args.show_deps or args.show_config: + if args.command in ('config', 'deps'): args.verbosity_level -= 1 bootstrap_logging(args) @@ -61,13 +70,12 @@ def main(): log_extension_info(installed_extensions, enabled_extensions) - # TODO: move to 'mopidy config' and 'mopidy deps' - if args.show_config: + if args.command == 'config': logger.info('Dumping sanitized user config and exiting.') print config_lib.format( config, installed_extensions, config_errors) sys.exit(0) - if args.show_deps: + elif args.command == 'deps': logger.info('Dumping debug info about dependencies and exiting.') print deps.format_dependency_list() sys.exit(0) diff --git a/mopidy/commands.py b/mopidy/commands.py index 36269dca..480e0f04 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -37,14 +37,6 @@ def build_parser(): '--save-debug-log', action='store_true', dest='save_debug_log', help='save debug log to "./mopidy.log"') - parser.add_argument( - '--show-config', - action='store_true', dest='show_config', - help='show current config') - parser.add_argument( - '--show-deps', - action='store_true', dest='show_deps', - help='show dependencies and their versions') parser.add_argument( '--config', action='store', dest='config_files', type=config_files_type, diff --git a/tests/help_test.py b/tests/help_test.py index 574e4fd7..75d545bc 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -22,6 +22,5 @@ class HelpTest(unittest.TestCase): self.assertIn('--quiet', output) self.assertIn('--verbose', output) self.assertIn('--save-debug-log', output) - self.assertIn('--show-config', output) self.assertIn('--config', output) self.assertIn('--option', output) From 4f036776757ad7dd8522fae066964624bf80a04a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:48:14 +0100 Subject: [PATCH 05/43] main: Improve main bootstrapping sequence - Parses args in two pases to allow for setup of logging well before doing extension sub-commands. --- mopidy/__main__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index bc5ca86c..32102b29 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -40,18 +40,20 @@ def main(): deps_parser = subparser.add_parser('deps', help='show dependencies') deps_parser.set_defaults(command='deps') - args = parser.parse_args(args=mopidy_args) + bootstrap_args = parser.parse_known_args(args=mopidy_args)[0] - if args.command in ('config', 'deps'): - args.verbosity_level -= 1 - - bootstrap_logging(args) + if bootstrap_args.command in ('config', 'deps'): + bootstrap_args.verbosity_level -= 1 + bootstrap_logging(bootstrap_args) try: create_file_structures() check_old_locations() installed_extensions = ext.load_extensions() + # TODO: install extension subcommands. + + args = parser.parse_args(args=mopidy_args) config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) From 25fedc77006fd60b28e8da5e6943fa1c4aa82515 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 18:47:55 +0100 Subject: [PATCH 06/43] loggin: Add DelayedHandler to root logger. The delayed handler: - Accepts and buffers logs until they are released. - Upon release the logs are re-posted to the root logger. - After release log records are ignored. This allows us to avoid the silly tricks we've been doing with parsing args and config early for the sake of bootstraping logging. Now we can just start logging and once the logging has been setup the messages are released and handled according to the correct settings. --- mopidy/__main__.py | 11 +++++++++-- mopidy/utils/log.py | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 32102b29..201c8826 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -21,12 +21,15 @@ from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib from mopidy.core import Core -from mopidy.utils import deps, log, path, process +from mopidy.utils import deps, log, path, process, versioning logger = logging.getLogger('mopidy.main') def main(): + log.bootstrap_delayed_logging() + logger.info('Starting Mopidy %s', versioning.get_version()) + signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) @@ -54,9 +57,14 @@ def main(): # TODO: install extension subcommands. args = parser.parse_args(args=mopidy_args) + if args.command in ('deps', 'config'): + args.verbosity_level -= 1 + config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) + log.setup_logging(config, args.verbosity_level, args.save_debug_log) + enabled_extensions = [] for extension in installed_extensions: if not ext.validate_extension(extension): @@ -92,7 +100,6 @@ def main(): # Read-only config from here on, please. proxied_config = config_lib.Proxy(config) - log.setup_log_levels(proxied_config) ext.register_gstreamer_elements(enabled_extensions) # Anything that wants to exit after this point must use diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 9f3c973d..f7877614 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -4,14 +4,40 @@ import logging import logging.config import logging.handlers -from . import versioning + +class DelayedHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + self._released = False + self._buffer = [] + + def handle(self, record): + if not self._released: + self._buffer.append(record) + + def release(self): + self._released = True + root = logging.getLogger('') + while self._buffer: + root.handle(self._buffer.pop(0)) + + +_delayed_handler = DelayedHandler() + + +def bootstrap_delayed_logging(): + root = logging.getLogger('') + root.setLevel(logging.DEBUG) + root.addHandler(_delayed_handler) def setup_logging(config, verbosity_level, save_debug_log): - setup_root_logger() setup_console_logging(config, verbosity_level) + setup_log_levels(config) + if save_debug_log: setup_debug_logging_to_file(config) + if hasattr(logging, 'captureWarnings'): # New in Python 2.7 logging.captureWarnings(True) @@ -19,8 +45,7 @@ def setup_logging(config, verbosity_level, save_debug_log): if config['logging']['config_file']: logging.config.fileConfig(config['logging']['config_file']) - logger = logging.getLogger('mopidy.utils.log') - logger.info('Starting Mopidy %s', versioning.get_version()) + _delayed_handler.release() def setup_log_levels(config): @@ -28,11 +53,6 @@ def setup_log_levels(config): logging.getLogger(name).setLevel(level) -def setup_root_logger(): - root = logging.getLogger('') - root.setLevel(logging.DEBUG) - - def setup_console_logging(config, verbosity_level): if verbosity_level < 0: log_level = logging.WARNING From 7144876dc5dbc69e8f167f89c3e4870d4def6410 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 18:53:52 +0100 Subject: [PATCH 07/43] main: Move default subparsers into commands module Also switches to using dest for storing the chosen sub-parser. --- mopidy/__main__.py | 18 ++---------------- mopidy/commands.py | 9 ++++++++- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 201c8826..a765548d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -33,26 +33,12 @@ def main(): signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) - parser = commands.build_parser() - subparser = parser.add_subparsers(title='commands', metavar='COMMAND') - - run_parser = subparser.add_parser('run', help='start mopidy server') - run_parser.set_defaults(command='run') - config_parser = subparser.add_parser('config', help='show current config') - config_parser.set_defaults(command='config') - deps_parser = subparser.add_parser('deps', help='show dependencies') - deps_parser.set_defaults(command='deps') - - bootstrap_args = parser.parse_known_args(args=mopidy_args)[0] - - if bootstrap_args.command in ('config', 'deps'): - bootstrap_args.verbosity_level -= 1 - bootstrap_logging(bootstrap_args) - try: create_file_structures() check_old_locations() + parser, subparser = commands.build_parser() + installed_extensions = ext.load_extensions() # TODO: install extension subcommands. diff --git a/mopidy/commands.py b/mopidy/commands.py index 480e0f04..9d65549e 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -47,4 +47,11 @@ def build_parser(): action='append', dest='config_overrides', type=config_override_type, help='`section/key=value` values to override config options') - return parser + subparser = parser.add_subparsers( + title='commands', metavar='COMMAND', dest='command') + + subparser.add_parser('run', help='start mopidy server') + subparser.add_parser('config', help='show current config') + subparser.add_parser('deps', help='show dependencies') + + return parser, subparser From 518cac5eab773afd56aa486bee1ec6e4bad56d08 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 21:59:43 +0100 Subject: [PATCH 08/43] ext/backends: Add get_sub_commands and BaseSubCommandProvider --- mopidy/backends/base.py | 31 +++++++++++++++++++++++++++++++ mopidy/ext.py | 8 ++++++++ 2 files changed, 39 insertions(+) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 6b980f06..9dc9befa 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -279,3 +279,34 @@ class BasePlaylistsProvider(object): *MUST be implemented by subclass.* """ raise NotImplementedError + + +class BaseSubCommandProvider(object): + """Sub-classes may optionally add arguments to the passed in parser. + + :param parser: parser you may add arguments to + :type parser: :class:`argparse.ArgumentParser` + """ + + name = None + """What the sub-command should be called. Will be run as ``mopidy NAME`` + + Example: ``scan`` + """ + + help = None + """Optional help text for the sub-command, will be displayed in help.""" + + def __init__(self, parser): + pass + + def run(self, args, config): + """Run the sub-command implemented by this provider. + + *MUST be implemented by subclass.* + + :param args: the argments object from argpase. + :param config: read only version of the mopidy config. + :returns: integer exit value for the process. + """ + raise NotImplementedError diff --git a/mopidy/ext.py b/mopidy/ext.py index c239c374..aa6b4cd0 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -87,6 +87,14 @@ class Extension(object): """ return [] + def get_sub_commands(self): + """List of sub-command classes + + :returns: list of + :class:`~mopidy.backends.base.BaseSubCommandProvider` subclasses + """ + return [] + def register_gstreamer_elements(self): """Hook for registering custom GStreamer elements From ef10c2e178e723d46457ef2dddef7cb7a491eebd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 22:04:26 +0100 Subject: [PATCH 09/43] main: Wire in actual execution of sub-commands. --- mopidy/__main__.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index a765548d..bce3804c 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -38,9 +38,13 @@ def main(): check_old_locations() parser, subparser = commands.build_parser() - installed_extensions = ext.load_extensions() - # TODO: install extension subcommands. + extension_sub_commands = {} + + for extension in installed_extensions: + for cls in extension.get_sub_commands(): + cmd_parser = subparser.add_parser(cls.name, help=cls.help) + extension_sub_commands[cls.name] = (extension, cls(cmd_parser)) args = parser.parse_args(args=mopidy_args) if args.command in ('deps', 'config'): @@ -86,11 +90,25 @@ def main(): # Read-only config from here on, please. proxied_config = config_lib.Proxy(config) - ext.register_gstreamer_elements(enabled_extensions) + if args.command in extension_sub_commands: + extension, cmd = extension_sub_commands[args.command] - # Anything that wants to exit after this point must use - # mopidy.utils.process.exit_process as actors have been started. - start(proxied_config, enabled_extensions) + if extension not in enabled_extensions: + parser.error('Can not run sub-command %s from the disabled ' + 'extension %s.' % (cmd.name, extension.ext_name)) + + sys.exit(cmd.run(args, proxied_config)) + + if args.command == 'run': + ext.register_gstreamer_elements(enabled_extensions) + + # Anything that wants to exit after this point must use + # mopidy.utils.process.exit_process as actors have been started. + start(proxied_config, enabled_extensions) + sys.exit(0) + + parser.error( + 'Unknown command %s, this should never happen.' % args.command) except KeyboardInterrupt: pass except Exception as ex: From 2581062ae033fbdf46f90c299f1a850c16994179 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:18:26 +0100 Subject: [PATCH 10/43] ext/backends: Add extensions to sub commands run --- mopidy/__main__.py | 4 +++- mopidy/backends/base.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index bce3804c..035e0ae9 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -97,7 +97,9 @@ def main(): parser.error('Can not run sub-command %s from the disabled ' 'extension %s.' % (cmd.name, extension.ext_name)) - sys.exit(cmd.run(args, proxied_config)) + logging.info('Running %s command provided by %s.', cmd.name, + extension.ext_name) + sys.exit(cmd.run(args, proxied_config, enabled_extensions)) if args.command == 'run': ext.register_gstreamer_elements(enabled_extensions) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 9dc9befa..2fb20d03 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -300,13 +300,14 @@ class BaseSubCommandProvider(object): def __init__(self, parser): pass - def run(self, args, config): + def run(self, args, config, extensions): """Run the sub-command implemented by this provider. *MUST be implemented by subclass.* :param args: the argments object from argpase. :param config: read only version of the mopidy config. + :param extensions: list of enabled extensions. :returns: integer exit value for the process. """ raise NotImplementedError From b5f8480eea08eac2206d22c62dabfbf4e9c84cfc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:20:19 +0100 Subject: [PATCH 11/43] local: Add 'mopidy scan' command via extension sub-commands. --- mopidy/backends/local/__init__.py | 4 ++ mopidy/backends/local/scan.py | 105 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 mopidy/backends/local/scan.py diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 6c66c70d..ad70279b 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -36,3 +36,7 @@ class Extension(ext.Extension): def get_library_updaters(self): from .library import LocalLibraryUpdateProvider return [LocalLibraryUpdateProvider] + + def get_sub_commands(self): + from .scan import ScanSubCommand + return [ScanSubCommand] diff --git a/mopidy/backends/local/scan.py b/mopidy/backends/local/scan.py new file mode 100644 index 00000000..ab62f81b --- /dev/null +++ b/mopidy/backends/local/scan.py @@ -0,0 +1,105 @@ +from __future__ import unicode_literals + +import logging +import os +import time + +from mopidy import exceptions +from mopidy.audio import scan +from mopidy.backends import base +from mopidy.utils import path + +logger = logging.getLogger('mopidy.backends.local.scan') + + +class ScanSubCommand(base.BaseSubCommandProvider): + name = b'scan' + help = b'scan local media files' + + def run(self, args, config, extensions): + media_dir = config['local']['media_dir'] + scan_timeout = config['local']['scan_timeout'] + excluded_file_extensions = config['local']['excluded_file_extensions'] + + updaters = {} + for e in extensions: + for updater_class in e.get_library_updaters(): + if updater_class and 'local' in updater_class.uri_schemes: + updaters[e.ext_name] = updater_class + + if not updaters: + logging.error('No usable library updaters found.') + return 1 + elif len(updaters) > 1: + logging.error('More than one library updater found. ' + 'Provided by: %s', ', '.join(updaters.keys())) + return 1 + + local_updater = updaters.values()[0](config) + + uris_library = set() + uris_update = set() + uris_remove = set() + + logging.info('Checking tracks from library.') + for track in local_updater.load(): + try: + # TODO: convert local to file uri / path + stat = os.stat(path.uri_to_path(track.uri)) + if int(stat.st_mtime) > track.last_modified: + uris_update.add(track.uri) + uris_library.add(track.uri) + except OSError: + logging.debug('Missing file %s', track.uri) + uris_remove.add(track.uri) + + logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) + for uri in uris_remove: + local_updater.remove(uri) + + logging.info('Checking %s for new or modified tracks.', media_dir) + for uri in path.find_uris(config['local']['media_dir']): + file_extension = os.path.splitext(path.uri_to_path(uri))[1] + if file_extension in excluded_file_extensions: + logging.debug('Skipped %s: File extension excluded.', uri) + continue + + if uri not in uris_library: + uris_update.add(uri) + + logging.info('Found %d new or modified tracks.', len(uris_update)) + logging.info('Scanning new and modified tracks.') + + scanner = scan.Scanner(scan_timeout) + progress = Progress(len(uris_update)) + + for uri in sorted(uris_update): + try: + data = scanner.scan(uri) + track = scan.audio_data_to_track(data) + local_updater.add(track) + logging.debug('Added %s', track.uri) + except exceptions.ScannerError as error: + logging.warning('Failed %s: %s', uri, error) + + progress.increment() + + logging.info('Commiting changes.') + local_updater.commit() + return 0 + + +# TODO: move to utils? +class Progress(object): + def __init__(self, total): + self.count = 0 + self.total = total + self.start = time.time() + + def increment(self): + self.count += 1 + if self.count % 1000 == 0 or self.count == self.total: + duration = time.time() - self.start + remainder = duration / self.count * (self.total - self.count) + logging.info('Scanned %d of %d files in %ds, ~%ds left.', + self.count, self.total, duration, remainder) From 1a3ff456f94ce9c257054e3f73f9596d0e414ac7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:21:26 +0100 Subject: [PATCH 12/43] scanner: Remove old scanner in favour of sub-commands --- mopidy/scanner.py | 151 ---------------------------------------------- setup.py | 1 - 2 files changed, 152 deletions(-) delete mode 100644 mopidy/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py deleted file mode 100644 index 30fb553b..00000000 --- a/mopidy/scanner.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import unicode_literals - -import argparse -import logging -import os -import sys -import time - -import gobject -gobject.threads_init() - -# Extract any command line arguments. This needs to be done before GStreamer is -# imported, so that GStreamer doesn't hijack e.g. ``--help``. -mopidy_args = sys.argv[1:] -sys.argv[1:] = [] - -from mopidy import config as config_lib, exceptions, ext -from mopidy.audio import scan -from mopidy.utils import log, path, versioning - - -def main(): - args = parse_args() - # TODO: support config files and overrides (shared from main?) - config_files = [b'/etc/mopidy/mopidy.conf', - b'$XDG_CONFIG_DIR/mopidy/mopidy.conf'] - config_overrides = [] - - # TODO: decide if we want to avoid this boilerplate some how. - # Initial config without extensions to bootstrap logging. - logging_config, _ = config_lib.load(config_files, [], config_overrides) - log.setup_root_logger() - log.setup_console_logging(logging_config, args.verbosity_level) - - extensions = ext.load_extensions() - config, errors = config_lib.load( - config_files, extensions, config_overrides) - log.setup_log_levels(config) - - if not config['local']['media_dir']: - logging.warning('Config value local/media_dir is not set.') - return - - if not config['local']['scan_timeout']: - logging.warning('Config value local/scan_timeout is not set.') - return - - # TODO: missing config error checking and other default setup code. - - updaters = {} - for e in extensions: - for updater_class in e.get_library_updaters(): - if updater_class and 'local' in updater_class.uri_schemes: - updaters[e.ext_name] = updater_class - - if not updaters: - logging.error('No usable library updaters found.') - return - elif len(updaters) > 1: - logging.error('More than one library updater found. ' - 'Provided by: %s', ', '.join(updaters.keys())) - return - - local_updater = updaters.values()[0](config) # TODO: switch to actor? - - media_dir = config['local']['media_dir'] - excluded_extensions = config['local']['excluded_file_extensions'] - - uris_library = set() - uris_update = set() - uris_remove = set() - - logging.info('Checking tracks from library.') - for track in local_updater.load(): - try: - # TODO: convert local to file uri / path - stat = os.stat(path.uri_to_path(track.uri)) - if int(stat.st_mtime) > track.last_modified: - uris_update.add(track.uri) - uris_library.add(track.uri) - except OSError: - logging.debug('Missing file %s', track.uri) - uris_remove.add(track.uri) - - logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) - for uri in uris_remove: - local_updater.remove(uri) - - logging.info('Checking %s for new or modified tracks.', media_dir) - for uri in path.find_uris(config['local']['media_dir']): - if os.path.splitext(path.uri_to_path(uri))[1] in excluded_extensions: - logging.debug('Skipped %s: File extension excluded.', uri) - continue - - if uri not in uris_library: - uris_update.add(uri) - - logging.info('Found %d new or modified tracks.', len(uris_update)) - logging.info('Scanning new and modified tracks.') - - scanner = scan.Scanner(config['local']['scan_timeout']) - progress = Progress(len(uris_update)) - - for uri in sorted(uris_update): - try: - data = scanner.scan(uri) - track = scan.audio_data_to_track(data) - local_updater.add(track) - logging.debug('Added %s', track.uri) - except exceptions.ScannerError as error: - logging.warning('Failed %s: %s', uri, error) - - progress.increment() - - logging.info('Commiting changes.') - local_updater.commit() - - -class Progress(object): - def __init__(self, total): - self.count = 0 - self.total = total - self.start = time.time() - - def increment(self): - self.count += 1 - if self.count % 1000 == 0 or self.count == self.total: - duration = time.time() - self.start - remainder = duration / self.count * (self.total - self.count) - logging.info('Scanned %d of %d files in %ds, ~%ds left.', - self.count, self.total, duration, remainder) - - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument( - '--version', action='version', - version='Mopidy %s' % versioning.get_version()) - parser.add_argument( - '-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_argument( - '-v', '--verbose', - action='count', default=1, dest='verbosity_level', - help='more output (debug level)') - return parser.parse_args(args=mopidy_args) - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index a448a029..511a16e8 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ setup( entry_points={ 'console_scripts': [ 'mopidy = mopidy.__main__:main', - 'mopidy-scan = mopidy.scanner:main', 'mopidy-convert-config = mopidy.config.convert:main', ], 'mopidy.ext': [ From 67c028c31e51a181451d9636432629b89dabe130 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:23:52 +0100 Subject: [PATCH 13/43] docs: Update docs with respect to sub-commands --- docs/api/backends.rst | 5 +++ docs/commands/mopidy-scan.rst | 59 -------------------------------- docs/commands/mopidy.rst | 63 +++++++++++++++++++++++------------ docs/conf.py | 7 ---- docs/contributing.rst | 4 +-- docs/ext/local.rst | 15 +++------ docs/running.rst | 4 +-- docs/troubleshooting.rst | 4 +-- 8 files changed, 57 insertions(+), 104 deletions(-) delete mode 100644 docs/commands/mopidy-scan.rst diff --git a/docs/api/backends.rst b/docs/api/backends.rst index ec78f250..a85feb03 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -39,6 +39,11 @@ Library provider .. autoclass:: mopidy.backends.base.BaseLibraryProvider :members: +Sub-command provider +==================== + +.. autoclass:: mopidy.backends.base.BaseSubCommandProvider + :members: Backend listener ================ diff --git a/docs/commands/mopidy-scan.rst b/docs/commands/mopidy-scan.rst deleted file mode 100644 index e8c25f77..00000000 --- a/docs/commands/mopidy-scan.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. _mopidy-scan-cmd: - -******************* -mopidy-scan command -******************* - -Synopsis -======== - -mopidy-scan - [-h] [--version] [-q] [-v] - - -Description -=========== - -Mopidy is a music server which can play music both from multiple sources, like -your local hard drive, radio streams, and from Spotify and SoundCloud. Searches -combines results from all music sources, and you can mix tracks from all -sources in your play queue. Your playlists from Spotify or SoundCloud are also -available for use. - -The ``mopidy-scan`` command is used to index a music library to make it -available for playback with ``mopidy``. - - -Options -======= - -.. program:: mopidy-scan - -.. cmdoption:: --version - - Show Mopidy's version number and exit. - -.. cmdoption:: -h, --help - - Show help message and exit. - -.. cmdoption:: -q, --quiet - - Show less output: warning level and higher. - -.. cmdoption:: -v, --verbose - - Show more output: debug level and higher. - - -See also -======== - -:ref:`mopidy(1) ` - - -Reporting bugs -============== - -Report bugs to Mopidy's issue tracker at - diff --git a/docs/commands/mopidy.rst b/docs/commands/mopidy.rst index df4766c3..c4a35973 100644 --- a/docs/commands/mopidy.rst +++ b/docs/commands/mopidy.rst @@ -8,8 +8,8 @@ Synopsis ======== mopidy - [-h] [--version] [-q] [-v] [--save-debug-log] [--show-config] - [--show-deps] [--config CONFIG_FILES] [-o CONFIG_OVERRIDES] + [-h] [--version] [-q] [-v] [--save-debug-log] [--config CONFIG_FILES] + [-o CONFIG_OVERRIDES] COMMAND ... Description @@ -21,7 +21,7 @@ combines results from all music sources, and you can mix tracks from all sources in your play queue. Your playlists from Spotify or SoundCloud are also available for use. -The ``mopidy`` command is used to start the server. +The ``mopidy run`` command is used to start the server. Options @@ -50,16 +50,6 @@ Options Save debug log to the file specified in the :confval:`logging/debug_file` config value, typically ``./mopidy.log``. -.. cmdoption:: --show-config - - Show the current effective config. All configuration sources are merged - together to show the effective document. Secret values like passwords are - masked out. Config for disabled extensions are not included. - -.. cmdoption:: --show-deps - - Show dependencies, their versions and installation location. - .. cmdoption:: --config Specify config file to use. To use multiple config files, separate them @@ -72,6 +62,37 @@ Options be provided multiple times. +Built in sub-commands +===================== + +.. cmdoption:: run + + Run the mopidy server. + +.. cmdoption:: config + + Show the current effective config. All configuration sources are merged + together to show the effective document. Secret values like passwords are + masked out. Config for disabled extensions are not included. + +.. cmdoption:: deps + + Show dependencies, their versions and installation location. + + +Extension sub-commands +====================== + +Additionally, extensions can provide extra sub-commands. See ``mopidy --help`` +for a list of what is availbale on your system and ``mopidy COMMAND --help`` +for command specific help. Sub-commands for disabled extensions will be listed, +but can not be run. + +.. cmdoption:: local + + Scan local media files present in your library. + + Files ===== @@ -88,34 +109,32 @@ Examples To start the music server, run:: - mopidy + mopidy run To start the server with an additional config file than can override configs set in the default config files, run:: - mopidy --config ./my-config.conf + mopidy --config ./my-config.conf run To start the server and change a config value directly on the command line, run:: - mopidy --option mpd/enabled=false + mopidy --option mpd/enabled=false run The :option:`--option` flag may be repeated multiple times to change multiple configs:: - mopidy -o mpd/enabled=false -o spotify/bitrate=320 + mopidy -o mpd/enabled=false -o spotify/bitrate=320 run -The :option:`--show-config` output shows the effect of the :option:`--option` -flags:: +``mopidy config`` output shows the effect of the :option:`--option` flags:: - mopidy -o mpd/enabled=false -o spotify/bitrate=320 --show-config + mopidy -o mpd/enabled=false -o spotify/bitrate=320 config See also ======== -:ref:`mopidy-scan(1) `, :ref:`mopidy-convert-config(1) -` +:ref:`mopidy-convert-config(1) ` Reporting bugs ============== diff --git a/docs/conf.py b/docs/conf.py index 77ee897e..eb23daf5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -141,13 +141,6 @@ man_pages = [ '', '1' ), - ( - 'commands/mopidy-scan', - 'mopidy-scan', - 'index music for playback with mopidy', - '', - '1' - ), ( 'commands/mopidy-convert-config', 'mopidy-convert-config', diff --git a/docs/contributing.rst b/docs/contributing.rst index 22df8ced..b25c5f6d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -70,9 +70,9 @@ repo. #. Now you can run the Mopidy command, and it will run using the code in the Git repo:: - mopidy + mopidy run - If you do any changes to the code, you'll just need to restart ``mopidy`` + If you do any changes to the code, you'll just need to restart ``mopidy run`` to see the changes take effect. diff --git a/docs/ext/local.rst b/docs/ext/local.rst index f6b281bd..79307ea4 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -71,7 +71,7 @@ music... Generating a tag cache ---------------------- -The program :command:`mopidy-scan` will scan the path set in the +The program :command:`mopidy scan` will scan the path set in the :confval:`local/media_dir` config value for any media files and build a MPD compatible ``tag_cache``. @@ -80,16 +80,11 @@ To make a ``tag_cache`` of your local music available for Mopidy: #. Ensure that the :confval:`local/media_dir` config value points to where your music is located. Check the current setting by running:: - mopidy --show-config + mopidy config -#. Scan your media library. The command outputs the ``tag_cache`` to - standard output, which means that you will need to redirect the output to a - file yourself:: +#. Scan your media library. The command writes the ``tag_cache`` to + the :confval:`local/tag_cache_file`:: - mopidy-scan > tag_cache - -#. Move the ``tag_cache`` file to the location - set in the :confval:`local/tag_cache_file` config value, or change the - config value to point to where your ``tag_cache`` file is. + mopidy scan #. Start Mopidy, find the music library in a client, and play some local music! diff --git a/docs/running.rst b/docs/running.rst index c96805e4..389f6917 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -4,10 +4,10 @@ Running Mopidy To start Mopidy, simply open a terminal and run:: - mopidy + mopidy run For a complete reference to the Mopidy commands and their command line options, -see :ref:`mopidy-cmd` and :ref:`mopidy-scan-cmd`. +see :ref:`mopidy-cmd`. When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to accept connections by any MPD client. Check out our non-exhaustive diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 7ee4d417..f344b1cf 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -28,7 +28,7 @@ accepted, but large logs should still be shared through a pastebin. Effective configuration ======================= -The command :option:`mopidy --show-config` will print your full effective +The command ``mopidy config`` will print your full effective configuration the way Mopidy sees it after all defaults and all config files have been merged into a single config document. Any secret values like passwords are masked out, so the output of the command should be safe to share @@ -38,7 +38,7 @@ with others for debugging. Installed dependencies ====================== -The command :option:`mopidy --show-deps` will list the paths to and versions of +The command ``mopidy deps`` will list the paths to and versions of any dependency Mopidy or the extensions might need to work. This is very useful data for checking that you're using the right versions, and that you're using the right installation if you have multiple installations of a dependency on From 3945e437de2be2ce1a0d88af0490385eeb2e1318 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:24:19 +0100 Subject: [PATCH 14/43] docs: Fix minor doc issues found during review - Adds autodoc for mopidy.audio.scan - Updates ``file://`` to ``local:`` - Minor language fix. --- docs/api/audio.rst | 7 +++++++ docs/ext/local.rst | 2 +- docs/extensiondev.rst | 2 +- mopidy/audio/scan.py | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/api/audio.rst b/docs/api/audio.rst index 2b9f6cc5..550ca890 100644 --- a/docs/api/audio.rst +++ b/docs/api/audio.rst @@ -28,3 +28,10 @@ Audio listener .. autoclass:: mopidy.audio.AudioListener :members: + + +Audio scanner +============= + +.. autoclass:: mopidy.audio.scan.Scanner + :members: diff --git a/docs/ext/local.rst b/docs/ext/local.rst index 79307ea4..2615856b 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -6,7 +6,7 @@ Mopidy-Local Extension for playing music from a local music archive. -This backend handles URIs starting with ``file:``. +This backend handles URIs starting with ``local:``. Known issues diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 428751de..9c8464f6 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -353,7 +353,7 @@ Example backend If you want to extend Mopidy to support new music and playlist sources, you want to implement a backend. A backend does not have access to Mopidy's core -API at all and got a bunch of interfaces to implement. +API at all and have a bunch of interfaces to implement. The skeleton of a backend would look like this. See :ref:`backend-api` for more details. diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 82803379..435cac87 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -14,6 +14,15 @@ from mopidy.utils import path class Scanner(object): + """ + Helper to get tags and other relevant info from URIs. + + :param timeout: timeout for scanning a URI in ms + :type event: int + :param min_duration: minimum duration of scanned URI in ms, -1 for all. + :type event: int + """ + def __init__(self, timeout=1000, min_duration=100): self.timeout_ms = timeout self.min_duration_ms = min_duration @@ -35,6 +44,13 @@ class Scanner(object): self.bus.set_flushing(True) def scan(self, uri): + """ + Scan the given uri collecting relevant metadata. + + :param uri: URI of the resource to scan. + :type event: string + :return: Dictionary of tags, duration, mtime and uri information. + """ try: self._setup(uri) data = self._collect() From 5cd0938e0de85df6356196ab02198f214f8b411c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:38:09 +0100 Subject: [PATCH 15/43] docs: Update changelog with sub-commands work --- docs/changelog.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 44ca89f7..5dd3aa67 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,22 @@ v0.17.0 (UNRELEASED) track artist. Album artist is now only populated if the scanned file got an explicit album artist set. +**Sub-commands** + +- Swtiched to sub-commands for mopidy this implies the following changes + (fixes :issue:`437`): + + ===================== ============= + Old command New command + ===================== ============= + mopidy mopidy run + mopidy --show-deps mopidy deps + mopidy --show-config mopidy config + mopidy-scan mopidy scan + +- Added hooks for extensions to create their own custom sub-commands and + converted ``mopidy-scan`` as first user of new API (Fixes :issue:`436`). + v0.16.1 (2013-11-02) ==================== From 4e6ebbe9556e3b2d82d5445e77e56891ab637824 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 18:20:22 +0100 Subject: [PATCH 16/43] local/docs: Update based on review comments - Bunch of typos and wording improvements from review. - Fixed mopidy.backends.local.scan botched merge. - Document and enforce that sub-command name needs to be bytes. --- docs/changelog.rst | 4 +-- docs/commands/mopidy.rst | 2 +- mopidy/__main__.py | 11 ++++---- mopidy/backends/base.py | 7 +++--- mopidy/backends/local/scan.py | 47 +++++++++++++++++++---------------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 115d4d26..c611c8b6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,8 +25,8 @@ v0.17.0 (UNRELEASED) **Sub-commands** -- Swtiched to sub-commands for mopidy this implies the following changes - (fixes :issue:`437`): +- Switched to sub-commands for the ``mopidy`` command , this implies the + following changes (fixes :issue:`437`): ===================== ============= Old command New command diff --git a/docs/commands/mopidy.rst b/docs/commands/mopidy.rst index c4a35973..c03a40cf 100644 --- a/docs/commands/mopidy.rst +++ b/docs/commands/mopidy.rst @@ -53,7 +53,7 @@ Options .. cmdoption:: --config Specify config file to use. To use multiple config files, separate them - with colon. The later files override the earlier ones if there's a + with a colon. The later files override the earlier ones if there's a conflict. .. cmdoption:: -o