edit: invoke editor during importer, on Items

* Initial draft for invoking the edit plugin during an importer session.
* Add prompt choices for editing the original file tags ("eDit") and
apply a candidate and then edit ("edit Candidates").
* Modify plugin (_get_fields, apply_data, edit_objects) so "path" can be
used as a reference field instead of "id", as the Items are not still on
the database when the plugin is invoked via the importer.
* Modify ImportTask.manipulate_files() with a temporary flag for writing
the item tags even if ASIS was selected.
This commit is contained in:
Diego Moreda 2016-01-28 14:07:10 +01:00
parent b7a2a42d9c
commit 98abe69520
2 changed files with 88 additions and 8 deletions

View file

@ -672,7 +672,8 @@ class ImportTask(BaseImportTask):
# old paths.
item.move(copy, link)
if write and self.apply:
# TODO: the EDIT_FLAG field is a hack!
if write and (self.apply or getattr(self, 'EDIT_FLAG', False)):
item.try_write()
with session.lib.transaction():

View file

@ -21,7 +21,9 @@ from beets import plugins
from beets import util
from beets import ui
from beets.dbcore import types
from beets.ui.commands import _do_query
from beets.importer import action
from beets.ui.commands import _do_query, PromptChoice
from copy import deepcopy
import subprocess
import yaml
from tempfile import NamedTemporaryFile
@ -151,6 +153,13 @@ class EditPlugin(plugins.BeetsPlugin):
'ignore_fields': 'id path',
})
self.register_listener('before_choose_candidate',
self.before_choose_candidate_event)
# Field to be used as "unequivocal, non-editable key" for an Item.
# TODO: cleanup
self.mapping_field = 'id'
def commands(self):
edit_command = ui.Subcommand(
'edit',
@ -202,8 +211,8 @@ class EditPlugin(plugins.BeetsPlugin):
if extra:
fields += extra
# Ensure we always have the `id` field for identification.
fields.append('id')
# Ensure we always have the mapping field for identification.
fields.append(self.mapping_field)
return set(fields)
@ -262,10 +271,21 @@ class EditPlugin(plugins.BeetsPlugin):
return False
# Show the changes.
# 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)
self.apply_data(objs, old_data, new_data)
changed = False
for obj in objs:
changed |= ui.show_model_changes(obj)
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))
else:
obj_old = None
changed |= ui.show_model_changes(obj, obj_old)
if not changed:
ui.print_('No changes to apply.')
return False
@ -295,11 +315,24 @@ 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_id = {o.id: o for o in objs}
obj_by_f = {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
@ -313,8 +346,9 @@ class EditPlugin(plugins.BeetsPlugin):
if forbidden:
continue
id = int(old_dict['id'])
apply(obj_by_id[id], new_dict)
# Reconcile back the user edits, using the mapping_field.
val = obj_from_ref(old_dict)
apply(obj_by_f[val], new_dict)
def save_changes(self, objs):
"""Save a list of updated Model objects to the database.
@ -324,3 +358,48 @@ class EditPlugin(plugins.BeetsPlugin):
if ob._dirty:
self._log.debug('saving changes to {}', ob)
ob.try_sync(ui.should_write(), ui.should_move())
# Methods for interactive importer execution.
def before_choose_candidate_event(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)]
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
else:
# Edit cancelled / no edits made. Revert changes.
for obj in task.items:
obj.read()
def importer_edit_candidate(self, session, task):
"""Callback for invoking the functionality during an interactive
import session on a *candidate* applied to the original items.
"""
# Prompt the user for a candidate, and simulate matching.
sel = ui.input_options([], numrange=(1, len(task.candidates)))
# Force applying the candidate on the items.
task.match = task.candidates[sel-1]
task.apply_metadata()
return self.importer_edit(session, task)