mirror of
https://github.com/beetbox/beets.git
synced 2026-01-30 20:13:37 +01:00
Merge pull request #965 from geigerzaehler/write-hook-mutate
Zero plugin can modify tags without changing the item
This commit is contained in:
commit
66f952bbb2
6 changed files with 99 additions and 35 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
Loading…
Reference in a new issue