edit: use action.RETAG, cleanup

* Make the edit plugin return action.RETAG when invoked during an
interactive import session, making the importer handle the writing of
the tags to the files (if needed) properly.
* Move the logic relative to the "reference field" to
_set_reference_field(), simplifying a bit the functions that depend on
this field.
* Hide the "edit Candidates" choice if no candidates are found.
This commit is contained in:
Diego Moreda 2016-02-01 18:57:03 +01:00
parent e23718dc0c
commit b8ec22ca34

View file

@ -154,11 +154,21 @@ class EditPlugin(plugins.BeetsPlugin):
})
self.register_listener('before_choose_candidate',
self.before_choose_candidate_event)
self.before_choose_candidate_listener)
self.register_listener('import_begin', self.import_begin_listener)
# Field to be used as "unequivocal, non-editable key" for an Item.
# TODO: cleanup
self.mapping_field = 'id'
def _set_reference_field(self, field):
"""Set the "unequivocal, non-editable field" that will be used for
reconciling back the user changes.
"""
if field == 'id':
self.reference_field = 'id'
self.ref_field_value = lambda o: int(o.id)
self.obj_from_ref = lambda d: int(d['id'])
elif field == 'path':
self.reference_field = 'path'
self.ref_field_value = lambda o: util.displayable_path(o.path)
self.obj_from_ref = lambda d: util.displayable_path(d['path'])
def commands(self):
edit_command = ui.Subcommand(
@ -183,6 +193,9 @@ class EditPlugin(plugins.BeetsPlugin):
def _edit_command(self, lib, opts, args):
"""The CLI command function for the `beet edit` command.
"""
# Set the reference field to "id", as all Models have valid ids.
self._set_reference_field('id')
# Get the objects to edit.
query = ui.decargs(args)
items, albums = _do_query(lib, query, opts.album, False)
@ -211,8 +224,8 @@ class EditPlugin(plugins.BeetsPlugin):
if extra:
fields += extra
# Ensure we always have the mapping field for identification.
fields.append(self.mapping_field)
# Ensure we always have the reference field for identification.
fields.append(self.reference_field)
return set(fields)
@ -274,15 +287,14 @@ class EditPlugin(plugins.BeetsPlugin):
# If the objects are not on the DB yet, we need a copy of their
# original state for show_model_changes.
if all(not obj.id for obj in objs):
objs_old = deepcopy(objs)
objs_old = {self.ref_field_value(obj): deepcopy(obj)
for obj in objs}
self.apply_data(objs, old_data, new_data)
changed = False
for obj in objs:
if not obj.id:
# TODO: remove uglyness
obj_old = next(x for x in objs_old if
getattr(x, self.mapping_field) ==
getattr(obj, self.mapping_field))
obj_old = objs_old[self.ref_field_value(obj)]
else:
obj_old = None
changed |= ui.show_model_changes(obj, obj_old)
@ -315,24 +327,11 @@ class EditPlugin(plugins.BeetsPlugin):
The objects are not written back to the database, so the changes
are temporary.
"""
# TODO: make this more pythonic
def ref_field_value(o):
if self.mapping_field == 'id':
return int(o.id)
elif self.mapping_field == 'path':
return util.displayable_path(o.path)
def obj_from_ref(d):
if self.mapping_field == 'id':
return int(d['id'])
elif self.mapping_field == 'path':
return util.displayable_path(d['path'])
if len(old_data) != len(new_data):
self._log.warn('number of objects changed from {} to {}',
len(old_data), len(new_data))
obj_by_f = {ref_field_value(o): o for o in objs}
obj_by_f = {self.ref_field_value(o): o for o in objs}
ignore_fields = self.config['ignore_fields'].as_str_seq()
for old_dict, new_dict in zip(old_data, new_data):
# Prohibit any changes to forbidden fields to avoid
@ -346,8 +345,8 @@ class EditPlugin(plugins.BeetsPlugin):
if forbidden:
continue
# Reconcile back the user edits, using the mapping_field.
val = obj_from_ref(old_dict)
# Reconcile back the user edits, using the reference_field.
val = self.obj_from_ref(old_dict)
apply(obj_by_f[val], new_dict)
def save_changes(self, objs):
@ -361,32 +360,35 @@ class EditPlugin(plugins.BeetsPlugin):
# Methods for interactive importer execution.
def before_choose_candidate_event(self, session, task):
def before_choose_candidate_listener(self, session, task):
"""Append an "Edit" choice to the interactive importer prompt.
"""
return [PromptChoice('d', 'eDit', self.importer_edit),
PromptChoice('c', 'edit Candidates',
self.importer_edit_candidate)]
choices = [PromptChoice('d', 'eDit', self.importer_edit)]
if task.candidates:
choices.append(PromptChoice('c', 'edit Candidates',
self.importer_edit_candidate))
return choices
def import_begin_listener(self, session):
"""Initialize the reference field to 'path', as during an interactive
import session Models do not have valid 'id's yet.
"""
self._set_reference_field('path')
def importer_edit(self, session, task):
"""Callback for invoking the functionality during an interactive
import session on the *original* item tags.
"""
# Make 'path' the mapping field, as the Items do not have ids yet.
# TODO: move to ~import_begin
self.mapping_field = 'path'
# Present the YAML to the user and let her change it.
fields = self._get_fields(album=None, extra=[])
success = self.edit_objects(task.items, fields)
# Save the new data.
if success:
# TODO: implement properly, this is a quick illustrative hack.
# If using Items, the operation is something like
# "use the *modified* Items AS-IS *and* write() them"
task.EDIT_FLAG = True
return action.ASIS
# Return action.RETAG, which makes the importer write the tags
# to the files if needed.
return action.RETAG
else:
# Edit cancelled / no edits made. Revert changes.
for obj in task.items: