From a5b37d01c94eb53bffaf14997205840f60692b71 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 17 Dec 2016 16:46:00 -0600 Subject: [PATCH 1/6] added zero subcommand --- beetsplug/zero.py | 54 ++++++++++++++++++++++++++++++++++- test/test_zero.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 1401b11de..403def807 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -21,7 +21,8 @@ import re from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action -from beets.util import confit +from beets.util import confit, syspath +from beets.ui import Subcommand, decargs import six __author__ = 'baobab@heresiarch.info' @@ -41,6 +42,7 @@ class ZeroPlugin(BeetsPlugin): self.import_task_choice_event) self.config.add({ + 'auto': True, 'fields': [], 'keep_fields': [], 'update_database': False, @@ -75,6 +77,16 @@ class ZeroPlugin(BeetsPlugin): if key in self.patterns: del self.patterns[key] + def commands(self): + zero_command = Subcommand('zero', help='set fields to null') + + def zero_fields(lib, opts, args): + for item in lib.items(decargs(args)): + self.process_item(item) + + zero_command.func = zero_fields + return [zero_command] + def validate_config(self, mode): """Check whether fields in the configuration are valid. @@ -120,10 +132,39 @@ class ZeroPlugin(BeetsPlugin): return True return False + def process_item(self, item): + if not self.patterns: + self._log.warning(u'no fields, nothing to do') + return + + for field, patterns in self.patterns.items(): + if field in item.keys(): + if not self.should_update(item, field): + continue + + value = item[field] + match = self.match_patterns(item[field], patterns) + + else: + value = '' + match = patterns is True + + if match: + self._log.warning(u'{0}: {1} -> None', field, value) + item[field] = None + if self.config['update_database']: + item.update({field: None}) + item.store() + + item.write() + def write_event(self, item, path, tags): """Set values in tags to `None` if the key and value are matched by `self.patterns`. """ + if not self.config['auto']: + return + if not self.patterns: self._log.warning(u'no fields, nothing to do') return @@ -141,3 +182,14 @@ class ZeroPlugin(BeetsPlugin): tags[field] = None if self.config['update_database']: item[field] = None + + def should_update(self, item, field): + media = MediaFile(syspath(item.path)) + attributes = media.fields() + if field in attributes: + return True + + if self.config['update_database'] and item[field]: + return True + + return False diff --git a/test/test_zero.py b/test/test_zero.py index fb28330ee..ca4a2dd67 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -121,6 +121,78 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): mediafile = MediaFile(syspath(item.path)) self.assertEqual(0, len(mediafile.images)) + def test_auto_false(self): + item = self.add_item_fixture(year=2000) + item.write() + mediafile = MediaFile(syspath(item.path)) + self.assertEqual(2000, mediafile.year) + + config['zero'] = { + 'fields': [u'year'], + 'update_database': True, + 'auto': False + } + self.load_plugins('zero') + + item.write() + mediafile = MediaFile(syspath(item.path)) + self.assertEqual(item['year'], 2000) + self.assertEqual(mediafile.year, 2000) + + def test_subcommand(self): + item = self.add_item_fixture( + year=2016, + day=13, + month=3, + comments=u'test comment' + ) + item.write() + item_id = item.id + config['zero'] = { + 'fields': [u'comments'], + 'update_database': True, + 'auto': False + } + self.load_plugins('zero') + self.run_command('zero') + + mediafile = MediaFile(syspath(item.path)) + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mediafile.year, 2016) + self.assertEqual(mediafile.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 + config['zero'] = { + 'fields': [u'comments'], + 'update_database': False, + 'auto': False + } + self.load_plugins('zero') + + z = ZeroPlugin() + z.debug = False + self.run_command('zero') + + mediafile = MediaFile(syspath(item.path)) + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mediafile.year, 2016) + self.assertEqual(item['comments'], u'test comment') + self.assertEqual(mediafile.comments, None) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From fec7e30fe18a441c4d5b18b640e122f92790f908 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 17 Dec 2016 17:17:55 -0600 Subject: [PATCH 2/6] added documentation for zero subcommand --- docs/plugins/zero.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/plugins/zero.rst b/docs/plugins/zero.rst index ecaa60a3d..282bb4fc8 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]`` From 148354c5c499bed665813a4631c5fd6f3e4e788a Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 17 Dec 2016 17:30:07 -0600 Subject: [PATCH 3/6] additional zero tests --- test/test_zero.py | 54 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/test/test_zero.py b/test/test_zero.py index ca4a2dd67..38fdb3726 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -139,7 +139,7 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): self.assertEqual(item['year'], 2000) self.assertEqual(mediafile.year, 2000) - def test_subcommand(self): + def test_subcommand_update_database_true(self): item = self.add_item_fixture( year=2016, day=13, @@ -164,7 +164,7 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): self.assertEqual(mediafile.comments, None) self.assertEqual(item['comments'], u'') - def test_subcommand_update_database_False(self): + def test_subcommand_update_database_false(self): item = self.add_item_fixture( year=2016, day=13, @@ -179,9 +179,6 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): 'auto': False } self.load_plugins('zero') - - z = ZeroPlugin() - z.debug = False self.run_command('zero') mediafile = MediaFile(syspath(item.path)) @@ -192,6 +189,53 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): self.assertEqual(item['comments'], u'test comment') self.assertEqual(mediafile.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() + + config['zero'] = { + 'fields': [u'comments'], + 'update_database': False, + 'auto': False + } + + self.load_plugins('zero') + self.run_command('zero', 'year: 2016') + + mediafile = MediaFile(syspath(item.path)) + + self.assertEqual(mediafile.year, 2016) + self.assertEqual(mediafile.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() + + config['zero'] = { + 'fields': [u'comments'], + 'update_database': False, + 'auto': False + } + + self.load_plugins('zero') + self.run_command('zero', 'year: 0000') + + mediafile = MediaFile(syspath(item.path)) + + self.assertEqual(mediafile.year, 2016) + self.assertEqual(mediafile.comments, u'test comment') def suite(): From c40a587e68d6dd6cb579183c4728d06a95090945 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 17 Dec 2016 17:49:45 -0600 Subject: [PATCH 4/6] update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 40b044941..b5ea8a62b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,10 @@ Changelog Changelog goes here! +Features: + +* :doc:`/plugins/zero`: Added ``zero`` command to manually trigger the zero + plugin. 1.4.2 (December 16, 2016) ------------------------- From 510fe6f146f1be757d321fae639fadb769f41986 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sun, 18 Dec 2016 14:27:42 -0600 Subject: [PATCH 5/6] refactor changes to zero plugin --- beetsplug/zero.py | 55 +++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 403def807..73961134a 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -21,7 +21,7 @@ import re from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action -from beets.util import confit, syspath +from beets.util import confit from beets.ui import Subcommand, decargs import six @@ -94,13 +94,13 @@ class ZeroPlugin(BeetsPlugin): the section of the configuration to validate. """ for field in self.config[mode].as_str_seq(): - if field not in MediaFile.fields(): - self._log.error(u'invalid field: {0}', field) - continue if mode == 'fields' and field in ('id', 'path', 'album_id'): self._log.warning(u'field \'{0}\' ignored, zeroing ' u'it would be dangerous', field) continue + if field not in MediaFile.fields(): + self._log.error(u'invalid field: {0}', field) + continue def set_pattern(self, field): """Set a field in `self.patterns` to a string list corresponding to @@ -133,30 +133,12 @@ class ZeroPlugin(BeetsPlugin): return False def process_item(self, item): - if not self.patterns: - self._log.warning(u'no fields, nothing to do') - return + tags = dict(item) - for field, patterns in self.patterns.items(): - if field in item.keys(): - if not self.should_update(item, field): - continue - - value = item[field] - match = self.match_patterns(item[field], patterns) - - else: - value = '' - match = patterns is True - - if match: - self._log.warning(u'{0}: {1} -> None', field, value) - item[field] = None - if self.config['update_database']: - item.update({field: None}) - item.store() - - item.write() + if self.set_fields(item, tags): + item.write(tags=tags) + if self.config['update_database']: + item.store(fields=tags) def write_event(self, item, path, tags): """Set values in tags to `None` if the key and value are matched @@ -165,9 +147,14 @@ class ZeroPlugin(BeetsPlugin): if not self.config['auto']: return + self.set_fields(item, tags) + + def set_fields(self, item, tags): + fields_set = False + if not self.patterns: self._log.warning(u'no fields, nothing to do') - return + return False for field, patterns in self.patterns.items(): if field in tags: @@ -178,18 +165,10 @@ class ZeroPlugin(BeetsPlugin): match = patterns is True 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 - def should_update(self, item, field): - media = MediaFile(syspath(item.path)) - attributes = media.fields() - if field in attributes: - return True - - if self.config['update_database'] and item[field]: - return True - - return False + return fields_set From 69a58ca842e9741a1125e5622e3d1c1c5b613ef9 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sun, 18 Dec 2016 14:28:55 -0600 Subject: [PATCH 6/6] additional test coverage for zero plugin --- test/test_zero.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/test_zero.py b/test/test_zero.py index 38fdb3726..e7af5fee8 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -19,6 +19,7 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): self.setup_beets() def tearDown(self): + self.remove_mediafile_fixtures() self.teardown_beets() self.unload_plugins() @@ -237,6 +238,76 @@ class ZeroPluginTest(unittest.TestCase, TestHelper): self.assertEqual(mediafile.year, 2016) self.assertEqual(mediafile.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 + config['zero'] = {} + self.load_plugins('zero') + 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() + mediafile = MediaFile(syspath(item.path)) + self.assertEqual(mediafile.year, 2016) + + item_id = item.id + config['zero'] = {'fields': [u'year'], + 'keep_fields': [u'comments']} + + self.load_plugins('zero') + self.run_command('zero') + + item = self.lib.get_item(item_id) + + self.assertEqual(item['year'], 2016) + self.assertEqual(mediafile.year, 2016) + + def test_keep_fields(self): + item = self.add_item_fixture(year=2016, comments=u'test comment') + config['zero'] = {'keep_fields': [u'year'], + 'fields': None, + 'update_database': True, + 'auto': 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): + config['zero'] = {'keep_fields': [u'year id'], + 'fields': None, + 'update_database': True, + 'auto': True} + + z = ZeroPlugin() + + self.assertNotIn('id', z.patterns) + + def test_field_preserved_tags(self): + config['zero'] = {'fields': [u'year id'], + 'update_database': True, + 'auto': True} + + z = ZeroPlugin() + + self.assertNotIn('id', z.patterns) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)