From 3d675b23394c47599934a0694a837bdeec4c0033 Mon Sep 17 00:00:00 2001 From: sahandKashani Date: Fri, 4 Apr 2014 09:28:53 +0200 Subject: [PATCH 1/4] bash completion documentation fix for GNU Bash-4.2 In GNU Bash-4.2, I have to put quotes around the eval statement for the shell to correctly interpret the output of the beet completion command. --- docs/reference/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From ee2cf0df8e1007c68ec89c2ca6e1474f2e087211 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 14:26:29 +0200 Subject: [PATCH 2/4] Plugins can't extend MediaFile anymore Backported from #607. See https://github.com/sampsyo/beets/pull/607#issuecomment-38387100 This will be return in #644. --- beets/plugins.py | 17 ----------------- docs/dev/plugins.rst | 35 ----------------------------------- 2 files changed, 52 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 6a58777cd..35fc314d1 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -19,7 +19,6 @@ import traceback from collections import defaultdict import beets -from beets import mediafile PLUGIN_NAMESPACE = 'beetsplug' @@ -40,7 +39,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 +84,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 +287,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 = [] 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^ From 766c8b190bb782a5ef7bafbfa657007d4f158872 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 14:56:10 +0200 Subject: [PATCH 3/4] Add path argument to item.write() Symmetrical to item.read(), this allows us to update the tags of files that are not in the library. The motivation was this issue [1]. The commit also includes a nasty hack in the plugin code that prevents breaking other plugins. [1]: https://github.com/geigerzaehler/beets-check/issues/1 --- beets/library.py | 14 ++++++++++---- beets/plugins.py | 7 ++++++- beetsplug/convert.py | 9 ++++----- 3 files changed, 20 insertions(+), 10 deletions(-) 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 35fc314d1..b6fb157ca 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -17,6 +17,7 @@ import logging import traceback from collections import defaultdict +import inspect import beets @@ -339,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. From 4970c1de5a6c42511f966618b0e2271dcf6d9ca5 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 15:19:32 +0200 Subject: [PATCH 4/4] Add tests for item.write() custom path --- test/test_library.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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__)