Repository: beetbox/beets

Issue:
zero: Permit stripping album art
It would be nice to have the option of automatically clearing embedded art when an item is imported.  Whether or not a media item actually contains embedded art, beets should ensure the resulting media item has no embedded art after being import.  There are two plugins which would offer a good place of implementation for this feature: the EmbedArt and the Zero plugins.

The EmbedArt plugin already supports a command called `clearart` which allows for the manual stripping of embedded art from items which match a query.  Since the the `clearart` operation is not automatic and there is no option for automation, an extra step is required on the importation of media.

What probably makes more sense is implementing support for the art field in the Zero plugin.  It can only be assumed that people who would use such a feature already have the Zero plugin deployed for clearing other fields.  That said, it would require less configuration as all a user would need to do is drop the art field in their configuration for the Zero plugin.  Moreover, with the EmbedArt plugin, it embeds art into media items by default.  This feature would need to be disabled in the configuration as well.



Discussion:
Seems quite reasonable. This would take the form of a special case in the zero plugin that would need access to the underlying MediaFile (and so would perhaps need to use a different plugin event).


Resolving patch:
diff --git a/beetsplug/zero.py b/beetsplug/zero.py
--- a/beetsplug/zero.py
+++ b/beetsplug/zero.py
@@ -17,7 +17,7 @@
 import re
 import logging
 from beets.plugins import BeetsPlugin
-from beets.library import Item
+from beets.mediafile import MediaFile
 from beets.importer import action
 from beets.util import confit
 
@@ -51,7 +51,7 @@ def __init__(self):
                 log.warn(u'[zero] field \'{0}\' ignored, zeroing '
                          u'it would be dangerous'.format(field))
                 continue
-            if field not in Item._fields.keys():
+            if field not in MediaFile.fields():
                 log.error(u'[zero] invalid field: {0}'.format(field))
                 continue
 
@@ -59,7 +59,7 @@ def __init__(self):
                 self.patterns[field] = self.config[field].as_str_seq()
             except confit.NotFoundError:
                 # Matches everything
-                self.patterns[field] = [u'']
+                self.patterns[field] = True
 
     def import_task_choice_event(self, session, task):
         """Listen for import_task_choice event."""
@@ -73,23 +73,29 @@ def match_patterns(cls, field, patterns):
         """Check if field (as string) is matching any of the patterns in
         the list.
         """
+        if patterns is True:
+            return True
         for p in patterns:
             if re.search(p, unicode(field), flags=re.IGNORECASE):
                 return True
         return False
 
     def write_event(self, item, path, tags):
-        """Listen for write event."""
+        """Set values in tags to `None` if the key and value are matched
+        by `self.patterns`.
+        """
         if not self.patterns:
             log.warn(u'[zero] no fields, nothing to do')
             return
 
         for field, patterns in self.patterns.items():
-            if field not in tags:
-                log.error(u'[zero] no such field: {0}'.format(field))
-                continue
-
-            value = tags[field]
-            if self.match_patterns(value, patterns):
+            if field in tags:
+                value = tags[field]
+                match = self.match_patterns(tags[field], patterns)
+            else:
+                value = ''
+                match = patterns is True
+
+            if match:
                 log.debug(u'[zero] {0}: {1} -> None'.format(field, value))
                 tags[field] = None


Test patch:
diff --git a/test/helper.py b/test/helper.py
--- a/test/helper.py
+++ b/test/helper.py
@@ -47,7 +47,7 @@
 from beets.library import Library, Item, Album
 from beets import importer
 from beets.autotag.hooks import AlbumInfo, TrackInfo
-from beets.mediafile import MediaFile
+from beets.mediafile import MediaFile, Image
 
 # TODO Move AutotagMock here
 import _common
@@ -346,18 +346,33 @@ def add_album_fixture(self, track_count=1, ext='mp3'):
             items.append(item)
         return self.lib.add_album(items)
 
-    def create_mediafile_fixture(self, ext='mp3'):
+    def create_mediafile_fixture(self, ext='mp3', images=[]):
         """Copies a fixture mediafile with the extension to a temporary
         location and returns the path.
 
         It keeps track of the created locations and will delete the with
         `remove_mediafile_fixtures()`
+
+        `images` is a subset of 'png', 'jpg', and 'tiff'. For each
+        specified extension a cover art image is added to the media
+        file.
         """
         src = os.path.join(_common.RSRC, 'full.' + ext)
         handle, path = mkstemp()
         os.close(handle)
         shutil.copyfile(src, path)
 
+        if images:
+            mediafile = MediaFile(path)
+            imgs = []
+            for img_ext in images:
+                img_path = os.path.join(_common.RSRC,
+                                        'image-2x3.{0}'.format(img_ext))
+                with open(img_path, 'rb') as f:
+                    imgs.append(Image(f.read()))
+            mediafile.images = imgs
+            mediafile.save()
+
         if not hasattr(self, '_mediafile_fixtures'):
             self._mediafile_fixtures = []
         self._mediafile_fixtures.append(path)
diff --git a/test/test_zero.py b/test/test_zero.py
--- a/test/test_zero.py
+++ b/test/test_zero.py
@@ -82,6 +82,20 @@ def test_do_not_change_database(self):
         self.assertEqual(item['year'], 2000)
         self.assertIsNone(mediafile.year)
 
+    def test_album_art(self):
+        path = self.create_mediafile_fixture(images=['jpg'])
+        item = Item.from_path(path)
+
+        mediafile = MediaFile(item.path)
+        self.assertNotEqual(0, len(mediafile.images))
+
+        config['zero'] = {'fields': ['images']}
+        self.load_plugins('zero')
+
+        item.write()
+        mediafile = MediaFile(item.path)
+        self.assertEqual(0, len(mediafile.images))
+
 
 def suite():
     return unittest.TestLoader().loadTestsFromName(__name__)