diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 5659207fe2..6a3013069a 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -23,6 +23,7 @@ from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action +from beets.ui import Subcommand, decargs, input_yn from beets.util import confit __author__ = 'baobab@heresiarch.info' @@ -39,6 +40,7 @@ def __init__(self): self.import_task_choice_event) self.config.add({ + 'auto': True, 'fields': [], 'keep_fields': [], 'update_database': False, @@ -63,6 +65,20 @@ def __init__(self): field not in ('id', 'path', 'album_id')): self._set_pattern(field) + def commands(self): + zero_command = Subcommand('zero', help='set fields to null') + + def zero_fields(lib, opts, args): + if not decargs(args) and not input_yn( + u"Remove fields for all items? (Y/n)", + True): + return + for item in lib.items(decargs(args)): + self.process_item(item) + + zero_command.func = zero_fields + return [zero_command] + def _set_pattern(self, field): """Set a field in `self.patterns` to a string list corresponding to the configuration, or `True` if the field has no specific @@ -93,24 +109,41 @@ def write_event(self, item, path, tags): """Set values in tags to `None` if the key and value are matched by `self.patterns`. """ + if self.config['auto']: + self.set_fields(item, tags) + + def set_fields(self, item, tags): + fields_set = False + if not self.fields_to_progs: self._log.warning(u'no fields, nothing to do') - return + return False for field, progs in self.fields_to_progs.items(): if field in tags: value = tags[field] - match = _match_progs(value, progs, self._log) + match = _match_progs(tags[field], progs, self._log) else: value = '' match = not progs if match: + fields_set = True self._log.debug(u'{0}: {1} -> None', field, value) tags[field] = None if self.config['update_database']: item[field] = None + return fields_set + + def process_item(self, item): + tags = dict(item) + + if self.set_fields(item, tags): + item.write(tags=tags) + if self.config['update_database']: + item.store(fields=tags) + def _match_progs(value, progs, log): """Check if field (as string) is matching any of the patterns in diff --git a/docs/changelog.rst b/docs/changelog.rst index 2720ffe193..9cdf298c0f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,10 @@ Fixes: * :doc:`/plugins/bpd`: Fix a crash on non-ASCII MPD commands. :bug:`2332` +Features: + +* :doc:`/plugins/zero`: Added ``zero`` command to manually trigger the zero + plugin. 1.4.2 (December 16, 2016) ------------------------- diff --git a/docs/plugins/zero.rst b/docs/plugins/zero.rst index ecaa60a3d1..282bb4fc8f 100644 --- a/docs/plugins/zero.rst +++ b/docs/plugins/zero.rst @@ -18,6 +18,7 @@ Configuration Make a ``zero:`` section in your configuration file. You can specify the fields to nullify and the conditions for nullifying them: +* Set ``auto`` to ``yes`` to null fields automatically on import. Default ``yes`` * Set ``fields`` to a whitespace-separated list of fields to change. You can get the list of all available fields by running ``beet fields``. In addition, the ``images`` field allows you to remove any images @@ -42,3 +43,9 @@ If a custom pattern is not defined for a given field, the field will be nulled unconditionally. Note that the plugin currently does not zero fields when importing "as-is". + +Manually Triggering Zero +------------------------ + +The ``zero`` command will invoke the zero plugin on items matching a query. Use the +command ``beet zero [QUERY]`` diff --git a/test/test_zero.py b/test/test_zero.py index 1223e5c9ed..025eaa5407 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -5,7 +5,7 @@ from __future__ import division, absolute_import, print_function import unittest -from test.helper import TestHelper +from test.helper import TestHelper, control_stdin from beets.library import Item from beetsplug.zero import ZeroPlugin @@ -105,8 +105,211 @@ def test_album_art(self): self.load_plugins('zero') item.write() - mediafile = MediaFile(syspath(path)) - self.assertEqual(0, len(mediafile.images)) + mf = MediaFile(syspath(path)) + self.assertEqual(0, len(mf.images)) + + def test_auto_false(self): + self.config['zero']['fields'] = ['year'] + self.config['zero']['update_database'] = True + self.config['zero']['auto'] = False + + item = self.add_item_fixture(year=2000) + item.write() + + self.load_plugins('zero') + item.write() + + self.assertEqual(item['year'], 2000) + + def test_subcommand_update_database_true(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + item.write() + item_id = item.id + self.config['zero']['fields'] = ['comments'] + self.config['zero']['update_database'] = True + self.config['zero']['auto'] = False + + self.load_plugins('zero') + with control_stdin('y'): + self.run_command('zero') + + mf = MediaFile(syspath(item.path)) + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mf.year, 2016) + self.assertEqual(mf.comments, None) + self.assertEqual(item['comments'], u'') + + def test_subcommand_update_database_false(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + item.write() + item_id = item.id + + self.config['zero']['fields'] = ['comments'] + self.config['zero']['update_database'] = False + self.config['zero']['auto'] = False + + self.load_plugins('zero') + with control_stdin('y'): + self.run_command('zero') + + mf = MediaFile(syspath(item.path)) + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mf.year, 2016) + self.assertEqual(item['comments'], u'test comment') + self.assertEqual(mf.comments, None) + + def test_subcommand_query_include(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + + item.write() + + self.config['zero']['fields'] = ['comments'] + self.config['zero']['update_database'] = False + self.config['zero']['auto'] = False + + self.load_plugins('zero') + self.run_command('zero', 'year: 2016') + + mf = MediaFile(syspath(item.path)) + + self.assertEqual(mf.year, 2016) + self.assertEqual(mf.comments, None) + + def test_subcommand_query_exclude(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + + item.write() + + self.config['zero']['fields'] = ['comments'] + self.config['zero']['update_database'] = False + self.config['zero']['auto'] = False + + self.load_plugins('zero') + self.run_command('zero', 'year: 0000') + + mf = MediaFile(syspath(item.path)) + + self.assertEqual(mf.year, 2016) + self.assertEqual(mf.comments, u'test comment') + + def test_no_fields(self): + item = self.add_item_fixture(year=2016) + item.write() + mediafile = MediaFile(syspath(item.path)) + self.assertEqual(mediafile.year, 2016) + + item_id = item.id + + self.load_plugins('zero') + with control_stdin('y'): + self.run_command('zero') + + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mediafile.year, 2016) + + def test_whitelist_and_blacklist(self): + item = self.add_item_fixture(year=2016) + item.write() + mf = MediaFile(syspath(item.path)) + self.assertEqual(mf.year, 2016) + + item_id = item.id + self.config['zero']['fields'] = [u'year'] + self.config['zero']['keep_fields'] = [u'comments'] + + self.load_plugins('zero') + with control_stdin('y'): + self.run_command('zero') + + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mf.year, 2016) + + def test_keep_fields(self): + item = self.add_item_fixture(year=2016, comments=u'test comment') + self.config['zero']['keep_fields'] = [u'year'] + self.config['zero']['fields'] = None + self.config['zero']['update_database'] = True + + tags = { + 'comments': u'test comment', + 'year': 2016, + } + self.load_plugins('zero') + + z = ZeroPlugin() + z.write_event(item, item.path, tags) + self.assertEqual(tags['comments'], None) + self.assertEqual(tags['year'], 2016) + + def test_keep_fields_removes_preserved_tags(self): + self.config['zero']['keep_fields'] = [u'year'] + self.config['zero']['fields'] = None + self.config['zero']['update_database'] = True + + z = ZeroPlugin() + + self.assertNotIn('id', z.fields_to_progs) + + def test_fields_removes_preserved_tags(self): + self.config['zero']['fields'] = [u'year id'] + self.config['zero']['update_database'] = True + + z = ZeroPlugin() + + self.assertNotIn('id', z.fields_to_progs) + + def test_empty_query_n_response_no_changes(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + item.write() + item_id = item.id + self.config['zero']['fields'] = ['comments'] + self.config['zero']['update_database'] = True + self.config['zero']['auto'] = False + + self.load_plugins('zero') + with control_stdin('n'): + self.run_command('zero') + + mf = MediaFile(syspath(item.path)) + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mf.year, 2016) + self.assertEqual(mf.comments, u'test comment') + self.assertEqual(item['comments'], u'test comment') def suite():