diff --git a/beets/library.py b/beets/library.py index 945594307..fe4d9e0b5 100644 --- a/beets/library.py +++ b/beets/library.py @@ -414,17 +414,23 @@ class Item(LibModel): self.path = read_path - def write(self): - """Write the item's metadata to the associated file. + def write(self, path=None): + """Write the item's metadata to a media file. + + ``path`` defaults to the item's path property. Can raise either a `ReadError` or a `WriteError`. """ + if path is None: + path = self.path + else: + path = normpath(path) try: - f = MediaFile(syspath(self.path)) + f = MediaFile(syspath(path)) except (OSError, IOError) as exc: raise ReadError(self.path, exc) - plugins.send('write', item=self) + plugins.send('write', item=self, path=path) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) diff --git a/beets/plugins.py b/beets/plugins.py index 6a58777cd..b6fb157ca 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -17,9 +17,9 @@ import logging import traceback from collections import defaultdict +import inspect import beets -from beets import mediafile PLUGIN_NAMESPACE = 'beetsplug' @@ -40,7 +40,6 @@ class BeetsPlugin(object): def __init__(self, name=None): """Perform one-time plugin setup. """ - _add_media_fields(self.item_fields()) self.import_stages = [] self.name = name or self.__module__.split('.')[-1] self.config = beets.config[self.name] @@ -86,14 +85,6 @@ class BeetsPlugin(object): """ return () - def item_fields(self): - """Returns field descriptors to be added to the MediaFile class, - in the form of a dictionary whose keys are field names and whose - values are descriptor (e.g., MediaField) instances. The Library - database schema is not (currently) extended. - """ - return {} - def album_for_id(self, album_id): """Return an AlbumInfo object or None if no matching release was found. @@ -297,13 +288,6 @@ def template_funcs(): funcs.update(plugin.template_funcs) return funcs -def _add_media_fields(fields): - """Adds a {name: descriptor} dictionary of fields to the MediaFile - class. Called during the plugin initialization. - """ - for key, value in fields.iteritems(): - setattr(mediafile.MediaFile, key, value) - def import_stages(): """Get a list of import stage functions defined by plugins.""" stages = [] @@ -356,4 +340,8 @@ def send(event, **arguments): Returns a list of return values from the handlers. """ log.debug('Sending event: %s' % event) - return [handler(**arguments) for handler in event_handlers()[event]] + for handler in event_handlers()[event]: + # Don't break legacy plugins if we want to pass more arguments + argspec = inspect.getargspec(handler).args + args = dict((k, v) for k, v in arguments.items() if k in argspec) + handler(**args) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index c9645ae5a..031b4d839 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -174,13 +174,12 @@ def convert_item(dest_dir, keep_new, path_formats): encode(item.path, dest) # Write tags from the database to the converted file. - if not keep_new: - item.path = dest - item.write() + item.write(path=dest) - # If we're keeping the transcoded file, read it again (after - # writing) to get new bitrate, duration, etc. if keep_new: + # If we're keeping the transcoded file, read it again (after + # writing) to get new bitrate, duration, etc. + item.path = dest item.read() item.store() # Store new path and audio data. diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index cdea8938a..78b716d0e 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -297,41 +297,6 @@ This field works for *item* templates. Similarly, you can register *album* template fields by adding a function accepting an ``Album`` argument to the ``album_template_fields`` dict. -Extend MediaFile -^^^^^^^^^^^^^^^^ - -`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":: - - 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') - ), - } - -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. - -.. _MediaFile: https://github.com/sampsyo/beets/wiki/MediaFile Add Import Pipeline Stages ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index d479dcbae..4ee434237 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -361,7 +361,7 @@ Beets includes support for shell command completion. The command ``beet completion`` prints out a `bash`_ 3.2 script; to enable completion put a line like this into your ``.bashrc`` or similar file:: - eval $(beet completion) + eval "$(beet completion)" Or, to avoid slowing down your shell startup time, you can pipe the ``beet completion`` output to a file and source that instead. diff --git a/test/test_library.py b/test/test_library.py index 3fa3d1173..41585d50b 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -29,6 +29,7 @@ import beets.library from beets import util from beets import plugins from beets import config +from beets.mediafile import MediaFile TEMP_LIB = os.path.join(_common.RSRC, 'test_copy.blb') @@ -958,6 +959,20 @@ class WriteTest(_common.LibTestCase): self.i.path = path self.assertRaises(beets.library.WriteError, self.i.write) + def test_write_with_custom_path(self): + custom_path = os.path.join(self.temp_dir, 'file.mp3') + self.i.path = os.path.join(self.temp_dir, 'item_file.mp3') + shutil.copy(os.path.join(_common.RSRC, 'empty.mp3'), custom_path) + shutil.copy(os.path.join(_common.RSRC, 'empty.mp3'), self.i.path) + + self.i['artist'] = 'new artist' + self.assertNotEqual(MediaFile(custom_path).artist, 'new artist') + self.assertNotEqual(MediaFile(self.i.path).artist, 'new artist') + + self.i.write(custom_path) + self.assertEqual(MediaFile(custom_path).artist, 'new artist') + self.assertNotEqual(MediaFile(self.i.path).artist, 'new artist') + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)