Repository: beetbox/beets

Issue:
badfiles: Check on import
How about running the badfiles-plugin on import, exactly like https://github.com/geigerzaehler/beets-check#usage-with-import (sans the checksum)?
Including the option to prevent import of a corrupted file.

Furthermore with at least mp3val an (automatic) fix could be possible?



Discussion:
A standard `auto` option sounds fine. Some thought will need to be put into choosing when to run the check and log messages without interrupting the main UI.

I think this can be like:

During import, check for badfiles, if there's any warning, run mp3val -f -nb OR whatever command user wants to.

Or it can be achieved whilst running beet bad command and setting autofix: yes in config.yaml 

I suggest adding a bad_import_action configuration option. It can take one of: ask, always, or never.

always: In this circumstance, likely the default, bad-files is never queried, and is thus faster and without any extra log messages interrupting the main UI.

never: In this circumstance, bad-files is ran, and if there are errors in any file, all files for the entire release are never imported. A single warning message for the release is displayed.

ask: In this circumstance, bad-files is ran, and for each track with errors, asks the user whether to [i]mport or [s]kip, with the output of bad-files displayed to aid them in choosing.
Yes, I came here looking for exactly this.  Perhaps a more general solution would be to add import hooks in general, so a number of plugins/commands can be run after the import process has completed?  I know that's sort of covered by auto: in a lot of plugins, but having it this way round would negate the need to have every plugin implement such a feature.  And also, especially with badfiles, having the option to -not- import if the files are bad would be a huge bonus.
Yeah, I've always though it would be nice to provide "templates" for plugins to easily offer the same functionality on import and via an explicit command. Any help with this refactoring would be greatly appreciated.

Resolving patch:
diff --git a/beets/ui/commands.py b/beets/ui/commands.py
--- a/beets/ui/commands.py
+++ b/beets/ui/commands.py
@@ -698,6 +698,19 @@ def choose_match(self, task):
         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
--- a/beetsplug/badfiles.py
+++ b/beetsplug/badfiles.py
@@ -30,6 +30,7 @@
 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 @@ def __init__(self):
         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 @@ def check_item(self, item):
         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 @@ def check_item(self, item):
                 )
             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',


Test patch: