Plugins can now extend MediaField

This commit is contained in:
Thomas Scholtes 2014-04-04 00:48:29 +02:00
parent 4b1a1e3d65
commit eb4c323bcb
6 changed files with 76 additions and 34 deletions

View file

@ -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

View file

@ -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

View file

@ -8,3 +8,4 @@ in hacking beets itself or creating plugins for it.
plugins
api
media_file

21
docs/dev/media_file.rst Normal file
View file

@ -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:

View file

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -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#'})