mirror of
https://github.com/beetbox/beets.git
synced 2026-01-14 20:24:36 +01:00
add possibility to select individual items to the remove CLI command
This commit is contained in:
parent
0ca2c4e311
commit
822bc1ce88
4 changed files with 89 additions and 21 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue