diff --git a/beets/mediafile.py b/beets/mediafile.py index c01c32424..874a211b9 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1265,6 +1265,25 @@ class MediaFile(object): if isinstance(descriptor, MediaField): yield property + @classmethod + def add_field(cls, name, descriptor): + """Add a field to store custom tags. + + ``name`` is the name of the property the field is accessed + through. It must not already exist for the class. If the name + coincides with the name of a property of ``Item`` it will be set + from the item in ``item.write()``. + + ``descriptor`` must be an instance of ``MediaField``. + """ + if not isinstance(descriptor, MediaField): + raise ValueError( + u'{} must be an instance of MediaField'.format(descriptor)) + if name in cls.__dict__: + raise ValueError( + u'property "{}" already exists on MediaField'.format(name)) + setattr(cls, name, descriptor) + def update(self, dict, id3v23=False): """Update tags from the dictionary and write them to the file. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 630cd64e4..441d867ff 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -23,8 +23,10 @@ import time import _common from _common import unittest -from beets.mediafile import MediaFile, Image -from beets.library import ITEM_KEYS_WRITABLE, ITEM_KEYS +from beets.mediafile import MediaFile, MediaField, Image, \ + MP3StorageStyle, StorageStyle, \ + MP4StorageStyle, ASFStorageStyle +from beets.library import ITEM_KEYS_WRITABLE, ITEM_KEYS, Item class ArtTestMixin(object): @@ -252,7 +254,50 @@ class GenreListTestMixin(object): self.assertItemsEqual(mediafile.genres, [u'the genre', u'another']) -class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, LazySaveTestMixin): +field_extension = MediaField( + MP3StorageStyle('TKEY'), + MP4StorageStyle('----:com.apple.iTunes:initialkey'), + StorageStyle('INITIALKEY'), + ASFStorageStyle('INITIALKEY'), +) +class ExtendedFieldTestMixin(object): + + def test_extended_field_write(self): + MediaFile.add_field('initialkey', field_extension) + + mediafile = self._mediafile_fixture('empty') + mediafile.initialkey = 'F#' + mediafile.save() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(mediafile.initialkey, 'F#') + delattr(MediaFile, 'initialkey') + + def test_extended_attribute_from_item(self): + MediaFile.add_field('initialkey', field_extension) + + mediafile = self._mediafile_fixture('empty') + self.assertEqual(mediafile.initialkey, '') + + item = Item(path=mediafile.path, initialkey='Gb') + item.write() + mediafile = MediaFile(mediafile.path) + self.assertEqual(mediafile.initialkey, 'Gb') + delattr(MediaFile, 'initialkey') + + def test_invalid_descriptor(self): + with self.assertRaises(ValueError) as cm: + MediaFile.add_field('somekey', True) + self.assertIn('must be an instance of MediaField', str(cm.exception)) + + def test_overwrite_property(self): + with self.assertRaises(ValueError) as cm: + MediaFile.add_field('artist', MediaField()) + self.assertIn('property "artist" already exists', str(cm.exception)) + + +class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, + LazySaveTestMixin, ExtendedFieldTestMixin): """Test writing and reading tags. Subclasses must set ``extension`` and ``audio_properties``. """ @@ -744,10 +789,6 @@ class MediaFieldTest(unittest.TestCase): fields.extend(('encoder', 'images', 'genres', 'albumtype')) self.assertItemsEqual(MediaFile.fields(), fields) - def test_item_keys_writable_migration(self): - keys_writable = set(MediaFile.fields()).intersection(ITEM_KEYS) - self.assertItemsEqual(keys_writable, ITEM_KEYS_WRITABLE) - def suite(): return unittest.TestLoader().loadTestsFromName(__name__)