diff --git a/beets/mediafile.py b/beets/mediafile.py index 3f207ef20..733eaec32 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -913,12 +913,14 @@ class MediaField(object): def __init__(self, *styles, **kwargs): """Creates a new MediaField. - - `styles`: `StorageStyle` instances that describe the strategy - for reading and writing the field in particular formats. - There must be at least one style for each possible file - format. + - `styles`: `StorageStyle` instances that describe the strategy + for reading and writing the field in particular formats. + There must be at least one style for each possible file + format. + - `out_type`: the type of the value that should be returned when - getting this property. + getting this property. + """ self.out_type = kwargs.get('out_type', unicode) self._styles = styles diff --git a/beets/plugins.py b/beets/plugins.py index 6a58777cd..6b177432f 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -106,6 +106,20 @@ class BeetsPlugin(object): """ return None + def add_media_field(self, name, descriptor): + """Add a field that is synchronized between media files and items. + + When a media field is added ``item.write()`` will set the name + property of the item's MediaFile to ``item[name]`` and save the + changes. Similarly ``item.read()`` will set ``item[name]`` to + the value of the name property of the media file. + + ``descriptor`` must be an instance of ``mediafile.MediaField``. + """ + # Defer impor to prevent circular dependency + from beets import library + mediafile.MediaFile.add_field(name, descriptor) + library.Item.media_fields.add(name) listeners = None diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 2579ecbf4..82651a781 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -8,3 +8,4 @@ in hacking beets itself or creating plugins for it. plugins api + media_file diff --git a/docs/dev/media_file.rst b/docs/dev/media_file.rst new file mode 100644 index 000000000..c703377d8 --- /dev/null +++ b/docs/dev/media_file.rst @@ -0,0 +1,21 @@ +.. _mediafile: + +MediaFile +--------- + +.. currentmodule:: beets.mediafile + +.. autoclass:: MediaFile + + .. automethod:: __init__ + .. automethod:: fields + .. automethod:: readable_fields + .. automethod:: save + .. automethod:: update + +.. autoclass:: MediaField + + .. automethod:: __init__ + +.. autoclass:: StorageStyle + :members: diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index cdea8938a..cda1ca29e 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -300,38 +300,37 @@ template fields by adding a function accepting an ``Album`` argument to the Extend MediaFile ^^^^^^^^^^^^^^^^ -`MediaFile`_ is the file tag abstraction layer that beets uses to make +:ref:`MediaFile` is the file tag abstraction layer that beets uses to make cross-format metadata manipulation simple. Plugins can add fields to MediaFile to extend the kinds of metadata that they can easily manage. -The ``item_fields`` method on plugins should be overridden to return a -dictionary whose keys are field names and whose values are descriptor objects -that provide the field in question. The descriptors should probably be -``MediaField`` instances (defined in ``beets.mediafile``). Here's an example -plugin that provides a meaningless new field "foo":: +The ``MediaFile`` class uses ``MediaField`` descriptors to provide +access to file tags. Have a look at the ``beets.mediafile`` source code +to learn how to use this descriptor class. If you have created a +descriptor you can add it through your plugins ``add_media_field()`` +method. - from beets import mediafile, plugins, ui - class FooPlugin(plugins.BeetsPlugin): - def item_fields(self): - return { - 'foo': mediafile.MediaField( - mp3 = mediafile.StorageStyle( - 'TXXX', id3_desc=u'Foo Field'), - mp4 = mediafile.StorageStyle( - '----:com.apple.iTunes:Foo Field'), - etc = mediafile.StorageStyle('FOO FIELD') - ), - } +.. automethod:: beets.plugins.BeetsPlugin.add_media_field -Later, the plugin can manipulate this new field by saying something like -``mf.foo = 'bar'`` where ``mf`` is a ``MediaFile`` instance. -Note that, currently, these additional fields are *only* applied to -``MediaFile`` itself. The beets library database schema and the ``Item`` class -are not extended, so the fields are second-class citizens. This may change -eventually. +Here's an example plugin that provides a meaningless new field "foo":: + + class FooPlugin(BeetsPlugin): + def __init__(self): + field = mediafile.MediaField( + mediafile.MP3DescStorageStyle(u'foo') + mediafile.StorageStyle(u'foo') + ) + self.add_media_field('foo', field) + + FooPlugin() + item = Item.from_path('/path/to/foo/tag.mp3') + assert item['foo'] == 'spam' + + item['foo'] == 'ham' + item.write() + # The "foo" tag of the file is now "ham" -.. _MediaFile: https://github.com/sampsyo/beets/wiki/MediaFile Add Import Pipeline Stages ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 4f8a1b14c..99fbd4cc4 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -27,6 +27,7 @@ from beets.mediafile import MediaFile, MediaField, Image, \ MP3StorageStyle, StorageStyle, \ MP4StorageStyle, ASFStorageStyle from beets.library import Item +from beets.plugins import BeetsPlugin class ArtTestMixin(object): @@ -263,7 +264,8 @@ field_extension = MediaField( class ExtendedFieldTestMixin(object): def test_extended_field_write(self): - MediaFile.add_field('initialkey', field_extension) + plugin = BeetsPlugin() + plugin.add_media_field('initialkey', field_extension) mediafile = self._mediafile_fixture('empty') mediafile.initialkey = 'F#' @@ -272,9 +274,11 @@ class ExtendedFieldTestMixin(object): mediafile = MediaFile(mediafile.path) self.assertEqual(mediafile.initialkey, 'F#') delattr(MediaFile, 'initialkey') + Item.media_fields.remove('initialkey') def test_write_extended_tag_from_item(self): - MediaFile.add_field('initialkey', field_extension) + plugin = BeetsPlugin() + plugin.add_media_field('initialkey', field_extension) mediafile = self._mediafile_fixture('empty') self.assertEqual(mediafile.initialkey, '') @@ -285,10 +289,11 @@ class ExtendedFieldTestMixin(object): self.assertEqual(mediafile.initialkey, 'Gb') delattr(MediaFile, 'initialkey') + Item.media_fields.remove('initialkey') def test_read_flexible_attribute_from_file(self): - MediaFile.add_field('initialkey', field_extension) - Item.media_fields.add('initialkey') + plugin = BeetsPlugin() + plugin.add_media_field('initialkey', field_extension) mediafile = self._mediafile_fixture('empty') mediafile.update({'initialkey': 'F#'})