From 663d91c4b29581f5b752037ec56ec648ff9d9c6c Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 5 Apr 2014 00:17:25 +0200 Subject: [PATCH 1/6] Delete tags from media files --- beets/mediafile.py | 39 +++++++++++++++++++++++++++++++++++++-- test/test_mediafile.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 29cb4f396..69b2ed674 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -342,8 +342,9 @@ class StorageStyle(object): describe more sophisticated translations or format-specific access strategies. - MediaFile uses a StorageStyle via two methods: ``get()`` and - ``set()``. It passes a Mutagen file object to each. + MediaFile uses a StorageStyle via three methods: ``get()``, + ``set()``, and ``delete()``. It passes a Mutagen file object to + each. Internally, the StorageStyle implements ``get()`` and ``set()`` using two steps that may be overridden by subtypes. To get a value, @@ -447,6 +448,12 @@ class StorageStyle(object): return value + def delete(self, mutagen_file): + """Remove the tag from the file. + """ + if self.key in mutagen_file: + del mutagen_file[self.key] + class ListStorageStyle(StorageStyle): """Abstract storage style that provides access to lists. @@ -572,6 +579,12 @@ class MP4TupleStorageStyle(MP4StorageStyle): items[self.index] = int(value) self.store(mutagen_file, items) + def delete(self, mutagen_file): + if self.index == 0: + super(MP4TupleStorageStyle, self).delete(mutagen_file) + else: + self.set(mutagen_file, None) + class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): pass @@ -725,6 +738,15 @@ class MP3DescStorageStyle(MP3StorageStyle): except IndexError: return None + def delete(self, mutagen_file): + frame = None + for frame in mutagen_file.tags.getall(self.key): + if frame.desc.lower() == self.description.lower(): + break + if frame is not None: + del mutagen_file[frame.HashKey] + + class MP3SlashPackStorageStyle(MP3StorageStyle): """Store value as part of pair that is serialized as a slash- @@ -752,6 +774,12 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): items.pop() # Do not store last value self.store(mutagen_file, '/'.join(map(unicode, items))) + def delete(self, mutagen_file): + if self.pack_pos == 0: + super(MP3SlashPackStorageStyle, self).delete(mutagen_file) + else: + self.set(mutagen_file, None) + class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): """Converts between APIC frames and ``Image`` instances. @@ -943,6 +971,10 @@ class MediaField(object): value = self._none_value() for style in self.styles(mediafile.mgfile): style.set(mediafile.mgfile, value) + + def __delete__(self, mediafile): + for style in self.styles(mediafile.mgfile): + style.delete(mediafile.mgfile) def _none_value(self): """Get an appropriate "null" value for this field's type. This @@ -1083,6 +1115,9 @@ class DateItemField(MediaField): items[self.item_pos] = value self.date_field._set_date_tuple(mediafile, *items) + def __delete__(self, mediafile): + self.__set__(mediafile, None) + class CoverArtField(MediaField): """A descriptor that provides access to the *raw image data* for the diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 1cc6c85d6..32574e801 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -580,6 +580,34 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.assertEqual(mediafile.year, 0) self.assertEqual(mediafile.date, datetime.date.min) + def test_delete_tag(self): + mediafile = self._mediafile_fixture('full') + + keys = self.full_initial_tags.keys() + keys.remove('art') + for key in keys: + self.assertIsNotNone(getattr(mediafile, key)) + delattr(mediafile, key) + + mediafile.save() + mediafile = MediaFile(mediafile.path) + + # TODO Eventually the tags should have None values + empty_tags = dict((k, v) for k, v in self.empty_tags.items() + if k in keys) + self.assertTags(mediafile, empty_tags) + + def test_delete_packed_total(self): + mediafile = self._mediafile_fixture('full') + + delattr(mediafile, 'tracktotal') + delattr(mediafile, 'disctotal') + + mediafile.save() + mediafile = MediaFile(mediafile.path) + self.assertEqual(mediafile.track, self.full_initial_tags['track']) + self.assertEqual(mediafile.disc, self.full_initial_tags['disc']) + def assertTags(self, mediafile, tags): errors = [] for key, value in tags.items(): From a114b679744899735d2622949413d08da6a1e582 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 5 Apr 2014 21:30:10 +0200 Subject: [PATCH 2/6] Remove all tags from empty.wma --- test/rsrc/empty.wma | Bin 23618 -> 23608 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/rsrc/empty.wma b/test/rsrc/empty.wma index 76ea8a235a84761f64d14cd7cff772a88f3c653b..b4874c14c4ab167dd7697f9d7e18f95530e3ba02 100644 GIT binary patch delta 41 ucmX@KgK@_WMj->WO{INl=LMJDWLU+JbS~!?<3yon>`Dw^u$hz5G8zC~I}Q^7 delta 50 wcmdn7gYnP~Mj->WO{INl=LMJDWLU+JbS{U5X`;|GK^+DNU}Av6jsGp90lGR4F8}}l From a6839603cd078db54487e5ff46886a2a6af78673 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 5 Apr 2014 21:59:06 +0200 Subject: [PATCH 3/6] Return None for missing tags. Instead of returning a special "None value" if a tag does not exist, we return none directly. --- beets/mediafile.py | 92 +++++++++------ test/test_mediafile.py | 217 +++++++++++++++++++----------------- test/test_mediafile_edge.py | 4 +- 3 files changed, 174 insertions(+), 139 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 69b2ed674..29c79b451 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -98,10 +98,11 @@ def _safe_cast(out_type, val): returned. out_type should be bool, int, or unicode; otherwise, the value is just passed through. """ + if val is None: + return None + if out_type == int: - if val is None: - return 0 - elif isinstance(val, int) or isinstance(val, float): + if isinstance(val, int) or isinstance(val, float): # Just a number. return int(val) else: @@ -116,30 +117,22 @@ def _safe_cast(out_type, val): return int(val) elif out_type == bool: - if val is None: + try: + # Should work for strings, bools, ints: + return bool(int(val)) + except ValueError: return False - else: - try: - # Should work for strings, bools, ints: - return bool(int(val)) - except ValueError: - return False elif out_type == unicode: - if val is None: - return u'' + if isinstance(val, str): + return val.decode('utf8', 'ignore') + elif isinstance(val, unicode): + return val else: - if isinstance(val, str): - return val.decode('utf8', 'ignore') - elif isinstance(val, unicode): - return val - else: - return unicode(val) + return unicode(val) elif out_type == float: - if val is None: - return 0.0 - elif isinstance(val, int) or isinstance(val, float): + if isinstance(val, int) or isinstance(val, float): return float(val) else: if not isinstance(val, basestring): @@ -517,9 +510,7 @@ class SoundCheckStorageStyleMixin(object): """ def get(self, mutagen_file): data = self.fetch(mutagen_file) - if data is None: - return 0 - else: + if data is not None: return _sc_decode(data)[self.index] def set(self, mutagen_file, value): @@ -570,7 +561,13 @@ class MP4TupleStorageStyle(MP4StorageStyle): return list(items) + [0] * (packing_length - len(items)) def get(self, mutagen_file): - return super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] + value = super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] + if value == 0: + # The values are always present and saved as integers. So we + # assume that "0" indicates it is not set. + return None + else: + return value def set(self, mutagen_file, value): if value is None: @@ -757,19 +754,22 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): self.pack_pos = pack_pos def _fetch_unpacked(self, mutagen_file): - data = self.fetch(mutagen_file) or '' - items = unicode(data).split('/') + data = self.fetch(mutagen_file) + if data: + items = unicode(data).split('/') + else: + items = [] packing_length = 2 return list(items) + [None] * (packing_length - len(items)) def get(self, mutagen_file): - return self._fetch_unpacked(mutagen_file)[self.pack_pos] or 0 + return self._fetch_unpacked(mutagen_file)[self.pack_pos] def set(self, mutagen_file, value): items = self._fetch_unpacked(mutagen_file) items[self.pack_pos] = value if items[0] is None: - items[0] = 0 + items[0] = '' if items[1] is None: items.pop() # Do not store last value self.store(mutagen_file, '/'.join(map(unicode, items))) @@ -1038,18 +1038,25 @@ class DateField(MediaField): def __get__(self, mediafile, owner=None): year, month, day = self._get_date_tuple(mediafile) + if not year: + return None try: return datetime.date( - year or datetime.MINYEAR, + year, month or 1, day or 1 ) except ValueError: # Out of range values. - return datetime.date.min + return None def __set__(self, mediafile, date): self._set_date_tuple(mediafile, date.year, date.month, date.day) + def __delete__(self, mediafile): + super(DateField, self).__delete__(mediafile) + if hasattr(self, '_year_field'): + self._year_field.__delete__(mediafile) + def _get_date_tuple(self, mediafile): """Get a 3-item sequence representing the date consisting of a year, month, and day number. Each number is either an integer or @@ -1057,8 +1064,11 @@ class DateField(MediaField): """ # Get the underlying data and split on hyphens. datestring = super(DateField, self).__get__(mediafile, None) - datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) - items = unicode(datestring).split('-') + if isinstance(datestring, basestring): + datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) + items = unicode(datestring).split('-') + else: + items = [] # Ensure that we have exactly 3 components, possibly by # truncating or padding. @@ -1071,14 +1081,22 @@ class DateField(MediaField): items[0] = self._year_field.__get__(mediafile) # Convert each component to an integer if possible. - return [_safe_cast(int, item) for item in items] + items_ = [] + for item in items: + try: + items_.append(int(item)) + except: + items_.append(None) + return items_ def _set_date_tuple(self, mediafile, year, month=None, day=None): """Set the value of the field given a year, month, and day number. Each number can be an integer or None to indicate an unset component. """ - date = [year or 0] + if year is None: + self.__delete__(mediafile) + date = [year] if month: date.append(month) if month and day: @@ -1099,6 +1117,7 @@ class DateField(MediaField): return DateItemField(self, 2) + class DateItemField(MediaField): """Descriptor that gets and sets constituent parts of a `DateField`: the month, day, or year. @@ -1139,6 +1158,9 @@ class CoverArtField(MediaField): else: mediafile.images = [] + def __delete__(self, mediafile): + delattr(mediafile, 'images') + class ImageListField(MediaField): """Descriptor to access the list of images embedded in tags. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 32574e801..9183aea9e 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -284,7 +284,7 @@ class ExtendedFieldTestMixin(object): plugin.add_media_field('initialkey', field_extension) mediafile = self._mediafile_fixture('empty') - self.assertEqual(mediafile.initialkey, '') + self.assertIsNone(mediafile.initialkey) item = Item(path=mediafile.path, initialkey='Gb') item.write() @@ -333,8 +333,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'composer': u'the composer', 'grouping': u'the grouping', 'year': 2001, - 'month': 0, - 'day': 0, + 'month': None, + 'day': None, 'date': datetime.date(2001, 1, 1), 'track': 2, 'tracktotal': 3, @@ -351,60 +351,57 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'label': u'the label', } - empty_tags = { - 'title': u'', - 'artist': u'', - 'album': u'', - 'genre': u'', - 'composer': u'', - 'grouping': u'', - 'year': 0, - 'month': 0, - 'day': 0, - 'date': datetime.date.min, - 'track': 0, - 'tracktotal': 0, - 'disc': 0, - 'disctotal': 0, - 'lyrics': u'', - 'comments': u'', - 'bpm': 0, - 'comp': False, - 'mb_trackid': u'', - 'mb_albumid': u'', - 'mb_artistid':u'', - 'art': None, - 'label': u'', - - # Additional, non-iTunes fields. - 'rg_track_peak': 0.0, - 'rg_track_gain': 0.0, - 'rg_album_peak': 0.0, - 'rg_album_gain': 0.0, - 'albumartist': u'', - 'mb_albumartistid': u'', - 'artist_sort': u'', - 'albumartist_sort': u'', - 'acoustid_fingerprint': u'', - 'acoustid_id': u'', - 'mb_releasegroupid': u'', - 'asin': u'', - 'catalognum': u'', - 'disctitle': u'', - 'script': u'', - 'language': u'', - 'country': u'', - 'albumstatus': u'', - 'media': u'', - 'albumdisambig': u'', - 'artist_credit': u'', - 'albumartist_credit': u'', - 'original_year': 0, - 'original_month': 0, - 'original_day': 0, - 'original_date': datetime.date.min, + tag_fields = { + 'title', + 'artist', + 'album', + 'genre', + 'composer', + 'grouping', + 'year', + 'month', + 'day', + 'date', + 'track', + 'tracktotal', + 'disc', + 'disctotal', + 'lyrics', + 'comments', + 'bpm', + 'comp', + 'mb_trackid', + 'mb_albumid', + 'mb_artistid', + 'art', + 'label', + 'rg_track_peak', + 'rg_track_gain', + 'rg_album_peak', + 'rg_album_gain', + 'albumartist', + 'mb_albumartistid', + 'artist_sort', + 'albumartist_sort', + 'acoustid_fingerprint', + 'acoustid_id', + 'mb_releasegroupid', + 'asin', + 'catalognum', + 'disctitle', + 'script', + 'language', + 'country', + 'albumstatus', + 'media', + 'albumdisambig', + 'artist_credit', + 'albumartist_credit', + 'original_year', + 'original_month', + 'original_day', + 'original_date', } - def setUp(self): self.temp_dir = tempfile.mkdtemp() @@ -427,7 +424,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, def test_read_empty(self): mediafile = self._mediafile_fixture('empty') - self.assertTags(mediafile, self.empty_tags) + for field in self.tag_fields: + self.assertIsNone(getattr(mediafile, field)) def test_write_empty(self): mediafile = self._mediafile_fixture('empty') @@ -508,8 +506,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, mediafile = MediaFile(mediafile.path) self.assertEqual(mediafile.year, 2001) - self.assertEqual(mediafile.month, 0) - self.assertEqual(mediafile.day, 0) + self.assertIsNone(mediafile.month) + self.assertIsNone(mediafile.day) self.assertEqual(mediafile.date, datetime.date(2001,1,1)) def test_write_dates(self): @@ -528,22 +526,6 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.assertEqual(mediafile.original_day, 30) self.assertEqual(mediafile.original_date, datetime.date(1999,12,30)) - def test_read_write_float_none(self): - mediafile = self._mediafile_fixture('full') - mediafile.rg_track_gain = None - mediafile.rg_track_peak = None - mediafile.original_year = None - mediafile.original_month = None - mediafile.original_day = None - mediafile.save() - - mediafile = MediaFile(mediafile.path) - self.assertEqual(mediafile.rg_track_gain, 0) - self.assertEqual(mediafile.rg_track_peak, 0) - self.assertEqual(mediafile.original_year, 0) - self.assertEqual(mediafile.original_month, 0) - self.assertEqual(mediafile.original_day, 0) - def test_write_packed(self): mediafile = self._mediafile_fixture('empty') @@ -563,39 +545,39 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.assertEqual(mediafile.disctotal, 5) mediafile.track = 10 - mediafile.tracktotal = None + delattr(mediafile, 'tracktotal') mediafile.disc = 10 - mediafile.disctotal = None + delattr(mediafile, 'disctotal') mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(mediafile.track, 10) - self.assertEqual(mediafile.tracktotal, 0) + self.assertEqual(mediafile.tracktotal, None) self.assertEqual(mediafile.disc, 10) - self.assertEqual(mediafile.disctotal, 0) + self.assertEqual(mediafile.disctotal, None) def test_unparseable_date(self): mediafile = self._mediafile_fixture('unparseable') - self.assertEqual(mediafile.year, 0) - self.assertEqual(mediafile.date, datetime.date.min) + self.assertIsNone(mediafile.date) + self.assertIsNone(mediafile.year) + self.assertIsNone(mediafile.month) + self.assertIsNone(mediafile.day) def test_delete_tag(self): mediafile = self._mediafile_fixture('full') keys = self.full_initial_tags.keys() - keys.remove('art') - for key in keys: + for key in set(keys) - {'art', 'month', 'day'}: self.assertIsNotNone(getattr(mediafile, key)) + for key in keys: delattr(mediafile, key) mediafile.save() mediafile = MediaFile(mediafile.path) - # TODO Eventually the tags should have None values - empty_tags = dict((k, v) for k, v in self.empty_tags.items() - if k in keys) - self.assertTags(mediafile, empty_tags) + for key in keys: + self.assertIsNone(getattr(mediafile, key)) def test_delete_packed_total(self): mediafile = self._mediafile_fixture('full') @@ -608,6 +590,38 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.assertEqual(mediafile.track, self.full_initial_tags['track']) self.assertEqual(mediafile.disc, self.full_initial_tags['disc']) + def test_delete_partial_date(self): + mediafile = self._mediafile_fixture('empty') + + mediafile.date = datetime.date(2001, 12, 3) + mediafile.save() + mediafile = MediaFile(mediafile.path) + self.assertIsNotNone(mediafile.date) + self.assertIsNotNone(mediafile.year) + self.assertIsNotNone(mediafile.month) + self.assertIsNotNone(mediafile.day) + + delattr(mediafile, 'month') + mediafile.save() + mediafile = MediaFile(mediafile.path) + self.assertIsNotNone(mediafile.date) + self.assertIsNotNone(mediafile.year) + self.assertIsNone(mediafile.month) + self.assertIsNone(mediafile.day) + + def test_delete_year(self): + mediafile = self._mediafile_fixture('full') + + self.assertIsNotNone(mediafile.date) + self.assertIsNotNone(mediafile.year) + + delattr(mediafile, 'year') + mediafile.save() + mediafile = MediaFile(mediafile.path) + self.assertIsNone(mediafile.date) + self.assertIsNone(mediafile.year) + + def assertTags(self, mediafile, tags): errors = [] for key, value in tags.items(): @@ -634,20 +648,19 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, """Return dictionary of tags, mapping tag names to values. """ tags = {} - if base is None: - base = self.empty_tags - for key, value in base.items(): - if key == 'art': - tags[key] = self.jpg_data - elif isinstance(value, unicode): - tags[key] = 'value\u2010%s' % key - elif isinstance(value, int): - tags[key] = 1 - elif isinstance(value, float): + for key in self.tag_fields: + if key.startswith('rg_'): + # ReplayGain is float tags[key] = 1.0 - elif isinstance(value, bool): - tags[key] = True + else: + tags[key] = 'value\u2010%s' % key + + for key in ['disc', 'disctotal', 'track', 'tracktotal', 'bpm']: + tags[key] = 1 + + tags['art'] = self.jpg_data + tags['comp'] = True date = datetime.date(2001, 4, 3) tags['date'] = date @@ -674,9 +687,9 @@ class PartialTestMixin(object): def test_read_track_without_total(self): mediafile = self._mediafile_fixture('partial') self.assertEqual(mediafile.track, 2) - self.assertEqual(mediafile.tracktotal, 0) + self.assertIsNone(mediafile.tracktotal) self.assertEqual(mediafile.disc, 4) - self.assertEqual(mediafile.disctotal, 0) + self.assertIsNone(mediafile.disctotal) class MP3Test(ReadWriteTestBase, PartialTestMixin, @@ -845,7 +858,7 @@ class MediaFieldTest(unittest.TestCase): self.assertTrue(hasattr(mediafile, field)) def test_known_fields(self): - fields = ReadWriteTestBase.empty_tags.keys() + fields = list(ReadWriteTestBase.tag_fields) fields.extend(('encoder', 'images', 'genres', 'albumtype')) self.assertItemsEqual(MediaFile.fields(), fields) diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index f87eaf132..03017430e 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -30,7 +30,7 @@ class EdgeTest(unittest.TestCase): emptylist = beets.mediafile.MediaFile( os.path.join(_common.RSRC, 'emptylist.mp3')) genre = emptylist.genre - self.assertEqual(genre, '') + self.assertEqual(genre, None) def test_release_time_with_space(self): # Ensures that release times delimited by spaces are ignored. @@ -215,7 +215,7 @@ class TypeTest(unittest.TestCase): def test_set_year_to_none(self): self.mf.year = None - self.assertEqual(self.mf.year, 0) + self.assertIsNone(self.mf.year) def test_set_track_to_none(self): self.mf.track = None From 52dc84f43aaa87e1f3fb2198cf5ab5b74c0a60ce Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 5 Apr 2014 22:09:19 +0200 Subject: [PATCH 4/6] Set literal not available in py26 --- test/test_mediafile.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 9183aea9e..060f7cf2c 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -351,7 +351,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'label': u'the label', } - tag_fields = { + tag_fields = [ 'title', 'artist', 'album', @@ -401,7 +401,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'original_month', 'original_day', 'original_date', - } + ] + def setUp(self): self.temp_dir = tempfile.mkdtemp() @@ -568,7 +569,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, mediafile = self._mediafile_fixture('full') keys = self.full_initial_tags.keys() - for key in set(keys) - {'art', 'month', 'day'}: + for key in set(keys) - set(['art', 'month', 'day']): self.assertIsNotNone(getattr(mediafile, key)) for key in keys: delattr(mediafile, key) From 90dbefabd387175d5a7f1d810ca731ee1bdb9e8d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 10 Apr 2014 01:17:29 +0200 Subject: [PATCH 5/6] flake8 fixes --- beets/mediafile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 29c79b451..84f1178c1 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -744,7 +744,6 @@ class MP3DescStorageStyle(MP3StorageStyle): del mutagen_file[frame.HashKey] - class MP3SlashPackStorageStyle(MP3StorageStyle): """Store value as part of pair that is serialized as a slash- separated string. @@ -971,7 +970,7 @@ class MediaField(object): value = self._none_value() for style in self.styles(mediafile.mgfile): style.set(mediafile.mgfile, value) - + def __delete__(self, mediafile): for style in self.styles(mediafile.mgfile): style.delete(mediafile.mgfile) @@ -1117,7 +1116,6 @@ class DateField(MediaField): return DateItemField(self, 2) - class DateItemField(MediaField): """Descriptor that gets and sets constituent parts of a `DateField`: the month, day, or year. From d44adea9d33e92b50f1f0b8156cf3e6fd013bcea Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 10 Apr 2014 14:35:37 +0200 Subject: [PATCH 6/6] Delete 'None' properties from media file --- beets/mediafile.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 84f1178c1..a1a020fc6 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1353,12 +1353,16 @@ class MediaFile(object): """Set all field values from a dictionary. For any key in `dict` that is also a field to store tags the - method retrieves the corresponding value from `dict` and - updates the `MediaFile`. + method retrieves the corresponding value from `dict` and updates + the `MediaFile`. If a key has the value `None`, the + corresponding property is deleted from the `MediaFile`. """ for field in self.fields(): if field in dict: - setattr(self, field, dict[field]) + if dict[field] is None: + delattr(self, field) + else: + setattr(self, field, dict[field]) # Field definitions.