add possibility to select individual items to the remove CLI command

This commit is contained in:
wisp3rwind 2020-07-13 12:42:16 +02:00
parent 0ca2c4e311
commit 822bc1ce88
4 changed files with 89 additions and 21 deletions

View file

@ -389,17 +389,19 @@ def input_yn(prompt, require=False):
return sel == u'y'
def input_select_objects(prompt, objs, rep):
def input_select_objects(prompt, objs, rep, prompt_all=None):
"""Prompt to user to choose all, none, or some of the given objects.
Return the list of selected objects.
`prompt` is the prompt string to use for each question (it should be
phrased as an imperative verb). `rep` is a function to call on each
object to print it out when confirming objects individually.
phrased as an imperative verb). If `prompt_all` is given, it is used
instead of `prompt` for the first (yes(/no/select) question.
`rep` is a function to call on each object to print it out when confirming
objects individually.
"""
choice = input_options(
(u'y', u'n', u's'), False,
u'%s? (Yes/no/select)' % prompt)
u'%s? (Yes/no/select)' % (prompt_all or prompt))
print() # Blank line.
if choice == u'y': # Yes.

View file

@ -1232,31 +1232,53 @@ def remove_items(lib, query, album, delete, force):
"""
# Get the matching items.
items, albums = _do_query(lib, query, album)
objs = albums if album else items
# Confirm file removal if not forcing removal.
if not force:
# Prepare confirmation with user.
print_()
album_str = u" in {} album{}".format(
len(albums), u's' if len(albums) > 1 else u''
) if album else ""
if delete:
fmt = u'$path - $title'
prompt = u'Really DELETE %i file%s (y/n)?' % \
(len(items), 's' if len(items) > 1 else '')
prompt = u'Really DELETE'
prompt_all = u'Really DELETE {} file{}{}'.format(
len(items), u's' if len(items) > 1 else u'', album_str
)
else:
fmt = u''
prompt = u'Really remove %i item%s from the library (y/n)?' % \
(len(items), 's' if len(items) > 1 else '')
prompt = u'Really remove from the library?'
prompt_all = u'Really remove {} item{}{} from the library?'.format(
len(items), u's' if len(items) > 1 else u'', album_str
)
# Helpers for printing affected items
def fmt_track(t):
ui.print_(format(t, fmt))
def fmt_album(a):
ui.print_()
for i in a.items():
fmt_track(i)
fmt_obj = fmt_album if album else fmt_track
# Show all the items.
for item in items:
ui.print_(format(item, fmt))
for o in objs:
fmt_obj(o)
# Confirm with user.
if not ui.input_yn(prompt, True):
return
objs = ui.input_select_objects(prompt, objs, fmt_obj,
prompt_all=prompt_all)
if not objs:
return
# Remove (and possibly delete) items.
with lib.transaction():
for obj in (albums if album else items):
for obj in objs:
obj.remove(delete)

View file

@ -230,10 +230,21 @@ remove
Remove music from your library.
This command uses the same :doc:`query <query>` syntax as the ``list`` command.
You'll be shown a list of the files that will be removed and asked to confirm.
By default, this just removes entries from the library database; it doesn't
touch the files on disk. To actually delete the files, use ``beet remove -d``.
If you do not want to be prompted to remove the files, use ``beet remove -f``.
By default, it just removes entries from the library database; it doesn't
touch the files on disk. To actually delete the files, use the ``-d`` flag.
When the ``-a`` flag is given, the command operates on albums instead of
individual tracks.
When you run the ``remove`` command, it prints a list of all
affected items in the library and asks for your permission before removing
them. You can then choose to abort (type `n`), confirm (`y`), or interactively
choose some of the items (`s`). In the latter case, the command will prompt you
for every matching item or album and invite you to type `y` to remove the
item/album, `n` to keep it or `q` to exit and only remove the items/albums
selected up to this point.
This option lets you choose precisely which tracks/albums to remove without
spending too much time to carefully craft a query.
If you do not want to be prompted at all, use the ``-f`` option.
.. _modify-cmd:

View file

@ -111,7 +111,7 @@ class ListTest(unittest.TestCase):
self.assertNotIn(u'the album', stdout.getvalue())
class RemoveTest(_common.TestCase):
class RemoveTest(_common.TestCase, TestHelper):
def setUp(self):
super(RemoveTest, self).setUp()
@ -122,8 +122,8 @@ class RemoveTest(_common.TestCase):
# Copy a file into the library.
self.lib = library.Library(':memory:', self.libdir)
item_path = os.path.join(_common.RSRC, b'full.mp3')
self.i = library.Item.from_path(item_path)
self.item_path = os.path.join(_common.RSRC, b'full.mp3')
self.i = library.Item.from_path(self.item_path)
self.lib.add(self.i)
self.i.move(operation=MoveOperation.COPY)
@ -153,6 +153,39 @@ class RemoveTest(_common.TestCase):
self.assertEqual(len(list(items)), 0)
self.assertFalse(os.path.exists(self.i.path))
def test_remove_items_select_with_delete(self):
i2 = library.Item.from_path(self.item_path)
self.lib.add(i2)
i2.move(operation=MoveOperation.COPY)
for s in ('s', 'y', 'n'):
self.io.addinput(s)
commands.remove_items(self.lib, u'', False, True, False)
items = self.lib.items()
self.assertEqual(len(list(items)), 1)
# FIXME: is the order of the items as queried by the remove command
# really deterministic?
self.assertFalse(os.path.exists(syspath(self.i.path)))
self.assertTrue(os.path.exists(syspath(i2.path)))
def test_remove_albums_select_with_delete(self):
a1 = self.add_album_fixture()
a2 = self.add_album_fixture()
path1 = a1.items()[0].path
path2 = a2.items()[0].path
items = self.lib.items()
self.assertEqual(len(list(items)), 3)
for s in ('s', 'y', 'n'):
self.io.addinput(s)
commands.remove_items(self.lib, u'', True, True, False)
items = self.lib.items()
self.assertEqual(len(list(items)), 2) # incl. the item from setUp()
# FIXME: is the order of the items as queried by the remove command
# really deterministic?
self.assertFalse(os.path.exists(syspath(path1)))
self.assertTrue(os.path.exists(syspath(path2)))
class ModifyTest(unittest.TestCase, TestHelper):