Merge pull request #965 from geigerzaehler/write-hook-mutate

Zero plugin can modify tags without changing the item
This commit is contained in:
Adrian Sampson 2014-09-17 12:02:01 -07:00
commit 66f952bbb2
6 changed files with 99 additions and 35 deletions

View file

@ -473,7 +473,8 @@ class Item(LibModel):
else:
path = normpath(path)
plugins.send('write', item=self, path=path)
tags = dict(self)
plugins.send('write', item=self, path=path, tags=tags)
try:
mediafile = MediaFile(syspath(path),
@ -481,7 +482,7 @@ class Item(LibModel):
except (OSError, IOError, UnreadableFileError) as exc:
raise ReadError(self.path, exc)
mediafile.update(self)
mediafile.update(tags)
try:
mediafile.save()
except (OSError, IOError, MutagenError) as exc:

View file

@ -78,18 +78,18 @@ class ZeroPlugin(BeetsPlugin):
return True
return False
def write_event(self, item):
def write_event(self, item, path, tags):
"""Listen for write event."""
if not self.patterns:
log.warn(u'[zero] no fields, nothing to do')
return
for field, patterns in self.patterns.items():
if field not in item.keys():
if field not in tags:
log.error(u'[zero] no such field: {0}'.format(field))
continue
value = item[field]
value = tags[field]
if self.match_patterns(value, patterns):
log.debug(u'[zero] {0}: {1} -> None'.format(field, value))
item[field] = None
tags[field] = None

View file

@ -68,6 +68,10 @@ Fixes:
this case.
* :doc:`/plugins/convert`: Fix filename extensions when converting
automatically.
* The ``write`` event allows plugins to change the tags that are
written to a media file.
* :doc:`/plugins/zero`: Do not delete database values, only media file
tags.
.. _discogs_client: https://github.com/discogs/discogs_client

View file

@ -143,11 +143,14 @@ currently available are:
or album's part) is removed from the library (even when its file is not
deleted from disk).
* *write*: called with an ``Item`` object just before a file's metadata is
written to disk (i.e., just before the file on disk is opened). Event
handlers may raise a ``library.FileOperationError`` exception to abort
the write operation. Beets will catch that exception, print an error
message and continue.
* *write*: called with an ``Item`` object, a ``path``, and a ``tags``
dictionary just before a file's metadata is written to disk (i.e.,
just before the file on disk is opened). Event handlers may change
the ``tags`` dictionary to customize the tags that are written to the
media file. Event handlers may also raise a
``library.FileOperationError`` exception to abort the write
operation. Beets will catch that exception, print an error message
and continue.
* *after_write*: called with an ``Item`` object after a file's metadata is
written to disk (i.e., just after the file on disk is closed).

View file

@ -14,16 +14,17 @@
from mock import patch
from _common import unittest
from helper import TestHelper
import helper
from beets import plugins
from beets.library import Item
from beets.dbcore import types
from beets.mediafile import MediaFile
class PluginTest(unittest.TestCase, TestHelper):
class TestHelper(helper.TestHelper):
def setUp(self):
def setup_plugin_loader(self):
# FIXME the mocking code is horrific, but this is the lowest and
# earliest level of the plugin mechanism we can hook into.
self._plugin_loader_patch = patch('beets.plugins.load_plugins')
@ -35,9 +36,22 @@ class PluginTest(unittest.TestCase, TestHelper):
load_plugins.side_effect = myload
self.setup_beets()
def tearDown(self):
def teardown_plugin_loader(self):
self._plugin_loader_patch.stop()
self.unload_plugins()
def register_plugin(self, plugin_class):
self._plugin_classes.add(plugin_class)
class ItemTypesTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_flex_field_type(self):
@ -64,8 +78,38 @@ class PluginTest(unittest.TestCase, TestHelper):
out = self.run_with_output('ls', 'rating:3..5')
self.assertNotIn('aaa', out)
def register_plugin(self, plugin_class):
self._plugin_classes.add(plugin_class)
class ItemWriteTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
class EventListenerPlugin(plugins.BeetsPlugin):
pass
self.event_listener_plugin = EventListenerPlugin
self.register_plugin(EventListenerPlugin)
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_change_tags(self):
def on_write(item=None, path=None, tags=None):
if tags['artist'] == 'XXX':
tags['artist'] = 'YYY'
self.register_listener('write', on_write)
item = self.add_item_fixture(artist='XXX')
item.write()
mediafile = MediaFile(item.path)
self.assertEqual(mediafile.artist, 'YYY')
def register_listener(self, event, func):
self.event_listener_plugin.register_listener(event, func)
def suite():

View file

@ -18,37 +18,38 @@ class ZeroPluginTest(unittest.TestCase, TestHelper):
self.unload_plugins()
def test_no_patterns(self):
i = Item(
comments='test comment',
day=13,
month=3,
year=2012,
)
tags = {
'comments': 'test comment',
'day': 13,
'month': 3,
'year': 2012,
}
z = ZeroPlugin()
z.debug = False
z.fields = ['comments', 'month', 'day']
z.patterns = {'comments': ['.'],
'month': ['.'],
'day': ['.']}
z.write_event(i)
self.assertEqual(i.comments, '')
self.assertEqual(i.day, 0)
self.assertEqual(i.month, 0)
self.assertEqual(i.year, 2012)
z.write_event(None, None, tags)
self.assertEqual(tags['comments'], None)
self.assertEqual(tags['day'], None)
self.assertEqual(tags['month'], None)
self.assertEqual(tags['year'], 2012)
def test_patterns(self):
i = Item(
comments='from lame collection, ripped by eac',
year=2012,
)
z = ZeroPlugin()
z.debug = False
z.fields = ['comments', 'year']
z.patterns = {'comments': 'eac lame'.split(),
'year': '2098 2099'.split()}
z.write_event(i)
self.assertEqual(i.comments, '')
self.assertEqual(i.year, 2012)
tags = {
'comments': 'from lame collection, ripped by eac',
'year': 2012,
}
z.write_event(None, None, tags)
self.assertEqual(tags['comments'], None)
self.assertEqual(tags['year'], 2012)
def test_delete_replaygain_tag(self):
path = self.create_mediafile_fixture()
@ -70,6 +71,17 @@ class ZeroPluginTest(unittest.TestCase, TestHelper):
self.assertIsNone(mediafile.rg_track_peak)
self.assertIsNone(mediafile.rg_track_gain)
def test_do_not_change_database(self):
item = self.add_item_fixture(year=2000)
mediafile = MediaFile(item.path)
config['zero'] = {'fields': ['year']}
self.load_plugins('zero')
item.write()
self.assertEqual(item['year'], 2000)
self.assertIsNone(mediafile.year)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)