diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index f718e4623..be47ba1cb 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -367,6 +367,31 @@ def input_yn(prompt, require=False): return sel == 'y' +def input_select_items(prompt, items, rep): + """Prompts the user to use all, none or some of the items + Will return the list of items the user selected + prompt: prompt to use for all and for selective choice + items: full list of items + rep: function which represents an item to the user + is called with the item as argument + function is responsive for newline at input + """ + out_items = [] + choice = input_options( + ('y', 'n', 's'), False, + '%s? (Yes/no/select)' % prompt) + print() # go to a new line + if choice == 'y': + out_items = items + elif choice == 's': + for item in items: + rep(item) + if input_yn('%s? (yes/no)' % prompt, True): + out_items.append(item) + print() # go to a new line + return out_items + + # Human output formatting. def human_bytes(size): diff --git a/beets/ui/commands.py b/beets/ui/commands.py index c6846e92f..192c920ef 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1347,13 +1347,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): .format(len(objs), 'album' if album else 'item')) changed = set() for obj in objs: - obj.update(mods) - for field in dels: - try: - del obj[field] - except KeyError: - pass - if ui.show_model_changes(obj): + if print_and_modify(obj, mods, dels): changed.add(obj) # Still something to do? @@ -1372,8 +1366,9 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): else: extra = '' - if not ui.input_yn('Really modify%s (Y/n)?' % extra): - return + changed = ui.input_select_items( + 'Really modify%s' % extra, changed, + lambda o: print_and_modify(o, mods, dels)) # Apply changes to database and files with lib.transaction(): @@ -1381,6 +1376,21 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): obj.try_sync(write, move) +def print_and_modify(obj, mods, dels): + """Print the modifications to an item + and return a bool indicating whether any changes were made + mods: modifications + dels: fields to delete + """ + obj.update(mods) + for field in dels: + try: + del obj[field] + except KeyError: + pass + return ui.show_model_changes(obj) + + def modify_parse_args(args): """Split the arguments for the modify subcommand into query parts, assignments (field=value), and deletions (field!). Returns the result as @@ -1439,7 +1449,7 @@ default_commands.append(modify_cmd) # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend): +def move_items(lib, dest, query, copy, album, pretend, confirm=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. @@ -1453,6 +1463,7 @@ def move_items(lib, dest, query, copy, album, pretend): objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)] action = 'Copying' if copy else 'Moving' + act = 'copy' if copy else 'move' entity = 'album' if album else 'item' log.info(u'{0} {1} {2}{3}.', action, len(objs), entity, 's' if len(objs) != 1 else '') @@ -1467,6 +1478,12 @@ def move_items(lib, dest, query, copy, album, pretend): show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: + if confirm: + objs = ui.input_select_items( + 'Really %s' % act, objs, + lambda o: show_path_changes( + [(o.path, o.destination(basedir=dest))])) + for obj in objs: log.debug(u'moving: {0}', util.displayable_path(obj.path)) @@ -1481,7 +1498,8 @@ def move_func(lib, opts, args): if not os.path.isdir(dest): raise ui.UserError('no such directory: %s' % dest) - move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend) + move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend, + opts.timid) move_cmd = ui.Subcommand( @@ -1497,7 +1515,12 @@ move_cmd.parser.add_option( ) move_cmd.parser.add_option( '-p', '--pretend', default=False, action='store_true', - help='show how files would be moved, but don\'t touch anything') + help='show how files would be moved, but don\'t touch anything' +) +move_cmd.parser.add_option( + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' +) move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) diff --git a/docs/changelog.rst b/docs/changelog.rst index d862a6641..c23597285 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,10 @@ New: their values. Thanks to :user:`GuilhermeHideki`. :bug:`1812` * The :ref:`fields-cmd` command now displays flexible attributes. Thanks to :user:`GuilhermeHideki`. :bug:`1818` +* The :ref:`modify-cmd` command lets you interactive select tracks to apply + changes. :bug:`1843` +* The :ref:`move-cmd` command accepts `-t`, `--timid` switch now to confirm + or interactive select tracks process. :bug:`1843` .. _Google Code-In: https://codein.withgoogle.com/ .. _AcousticBrainz: http://acousticbrainz.org/ diff --git a/test/test_ui.py b/test/test_ui.py index 4a10a7947..480e6ce03 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -231,6 +231,22 @@ class ModifyTest(unittest.TestCase, TestHelper): item.load() self.assertEqual(0, item.mtime) + def test_selective_modify(self): + title = "Tracktitle" + album = "album" + origArtist = "composer" + newArtist = "coverArtist" + for i in range(0, 10): + self.add_item_fixture(title="{0}{1}".format(title, i), + artist=origArtist, + album=album) + self.modify_inp('s\ny\ny\ny\nn\nn\ny\ny\ny\ny\nn', + title, "artist={0}".format(newArtist)) + origItems = self.lib.items("artist:{0}".format(origArtist)) + newItems = self.lib.items("artist:{0}".format(newArtist)) + self.assertEqual(len(list(origItems)), 3) + self.assertEqual(len(list(newItems)), 7) + # Album Tests def test_modify_album(self): diff --git a/test/test_ui_init.py b/test/test_ui_init.py index d692868be..6ab94f9ea 100644 --- a/test/test_ui_init.py +++ b/test/test_ui_init.py @@ -21,6 +21,56 @@ from test._common import unittest from beets import ui +class InputMethodsTest(_common.TestCase): + def setUp(self): + super(InputMethodsTest, self).setUp() + self.io.install() + + def _print_helper(self, s): + print(s) + + def _print_helper2(self, s, prefix): + print(prefix, s) + + def test_input_select_items(self): + full_items = ['1', '2', '3', '4', '5'] + + # Test no + self.io.addinput('n') + items = ui.input_select_items( + "Prompt", full_items, self._print_helper) + self.assertEqual(items, []) + + # Test yes + self.io.addinput('y') + items = ui.input_select_items( + "Prompt", full_items, self._print_helper) + self.assertEqual(items, full_items) + + # Test selective 1 + self.io.addinput('s') + self.io.addinput('n') + self.io.addinput('y') + self.io.addinput('n') + self.io.addinput('y') + self.io.addinput('n') + items = ui.input_select_items( + "Prompt", full_items, self._print_helper) + self.assertEqual(items, ['2', '4']) + + # Test selective 2 + self.io.addinput('s') + self.io.addinput('y') + self.io.addinput('y') + self.io.addinput('n') + self.io.addinput('y') + self.io.addinput('n') + items = ui.input_select_items( + "Prompt", full_items, + lambda s: self._print_helper2(s, "Prefix")) + self.assertEqual(items, ['1', '2', '4']) + + class InitTest(_common.LibTestCase): def setUp(self): super(InitTest, self).setUp()