diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 4d010f4b1..38687b3a9 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -698,6 +698,19 @@ class TerminalImportSession(importer.ImportSession): print_(displayable_path(task.paths, u'\n') + u' ({0} items)'.format(len(task.items))) + # Let plugins display info or prompt the user before we go through the + # process of selecting candidate. + results = plugins.send('import_task_before_choice', + session=self, task=task) + actions = [action for action in results if action] + + if len(actions) == 1: + return actions[0] + elif len(actions) > 1: + raise plugins.PluginConflictException( + u'Only one handler for `import_task_before_choice` may return ' + u'an action.') + # Take immediate action if appropriate. action = _summary_judgment(task.rec) if action == importer.action.APPLY: diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 36b45de3a..5eaecc054 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -30,6 +30,7 @@ from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets.util import displayable_path, par_map from beets import ui +from beets import importer class CheckerCommandException(Exception): @@ -54,6 +55,11 @@ class BadFiles(BeetsPlugin): super(BadFiles, self).__init__() self.verbose = False + self.register_listener('import_task_start', + self.on_import_task_start) + self.register_listener('import_task_before_choice', + self.on_import_task_before_choice) + def run_command(self, cmd): self._log.debug(u"running command: {}", displayable_path(list2cmdline(cmd))) @@ -115,7 +121,7 @@ class BadFiles(BeetsPlugin): if not checker: self._log.error(u"no checker specified in the config for {}", ext) - return + return [] path = item.path if not isinstance(path, six.text_type): path = item.path.decode(sys.getfilesystemencoding()) @@ -130,25 +136,75 @@ class BadFiles(BeetsPlugin): ) else: self._log.error(u"error invoking {}: {}", e.checker, e.msg) - return + return [] + + error_lines = [] + if status > 0: - ui.print_(u"{}: checker exited with status {}" - .format(ui.colorize('text_error', dpath), status)) + error_lines.append( + u"{}: checker exited with status {}" + .format(ui.colorize('text_error', dpath), status)) for line in output: - ui.print_(u" {}".format(line)) + error_lines.append(u" {}".format(line)) + elif errors > 0: - ui.print_(u"{}: checker found {} errors or warnings" - .format(ui.colorize('text_warning', dpath), errors)) + error_lines.append( + u"{}: checker found {} errors or warnings" + .format(ui.colorize('text_warning', dpath), errors)) for line in output: - ui.print_(u" {}".format(line)) + error_lines.append(u" {}".format(line)) elif self.verbose: - ui.print_(u"{}: ok".format(ui.colorize('text_success', dpath))) + error_lines.append( + u"{}: ok".format(ui.colorize('text_success', dpath))) + + return error_lines + + def on_import_task_start(self, task, session): + if not self.config['check_on_import'].get(False): + return + + checks_failed = [] + + for item in task.items: + error_lines = self.check_item(item) + if error_lines: + checks_failed.append(error_lines) + + if checks_failed: + task._badfiles_checks_failed = checks_failed + + def on_import_task_before_choice(self, task, session): + if hasattr(task, '_badfiles_checks_failed'): + ui.print_('{} one or more files failed checks:' + .format(ui.colorize('text_warning', 'BAD'))) + for error in task._badfiles_checks_failed: + for error_line in error: + ui.print_(error_line) + + ui.print_() + ui.print_('What would you like to do?') + + sel = ui.input_options(['aBort', 'skip', 'continue']) + + if sel == 's': + return importer.action.SKIP + elif sel == 'c': + return None + elif sel == 'b': + raise importer.ImportAbort() + else: + raise Exception('Unexpected selection: {}'.format(sel)) def command(self, lib, opts, args): # Get items from arguments items = lib.items(ui.decargs(args)) self.verbose = opts.verbose - par_map(self.check_item, items) + + def check_and_print(item): + for error_line in self.check_item(item): + ui.print_(error_line) + + par_map(check_and_print, items) def commands(self): bad_command = Subcommand('bad', diff --git a/docs/changelog.rst b/docs/changelog.rst index 79ff35529..d9e30e1fd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -210,6 +210,8 @@ Other new things: * The :doc:`/plugins/aura` has arrived! * :doc:`plugins/fetchart`: The new ``max_filesize`` option for fetchart can be used to target a maximum image filesize. +* :doc:`/plugins/badfiles`: Checkers can now be run during import with the + ``check_on_import`` config option. Fixes: diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index a6aa3d6d7..d81461f4d 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -200,6 +200,13 @@ The events currently available are: import pipeline stage is a better choice (see :ref:`plugin-stage`). Parameters: ``task`` and ``session``. +* `import_task_before_choice`: called after candidate search for an import task + before any decision is made about how/if to import or tag. Can be used to + present information about the task or initiate interaction with the user + before importing occurs. Return an importer action to take a specific action. + Only one handler may return a non-None result. + Parameters: ``task`` and ``session`` + * `import_task_choice`: called after a decision has been made about an import task. This event can be used to initiate further interaction with the user. Use ``task.choice_flag`` to determine or change the action to be diff --git a/docs/plugins/badfiles.rst b/docs/plugins/badfiles.rst index a59dbd0d1..796f991e1 100644 --- a/docs/plugins/badfiles.rst +++ b/docs/plugins/badfiles.rst @@ -17,6 +17,7 @@ install yourself: You can also add custom commands for a specific extension, like this:: badfiles: + check_on_import: yes commands: ogg: myoggchecker --opt1 --opt2 flac: flac --test --warnings-as-errors --silent @@ -25,6 +26,10 @@ Custom commands will be run once for each file of the specified type, with the path to the file as the last argument. Commands must return a status code greater than zero for a file to be considered corrupt. +You can run the checkers when importing files by using the `check_on_import` +option. When on, checkers will be run against every imported file and warnings +and errors will be presented when selecting a tagging option. + .. _mp3val: http://mp3val.sourceforge.net/ .. _flac: https://xiph.org/flac/