From 2d77861e4d6867093873c516e8ca39bea94f5f34 Mon Sep 17 00:00:00 2001 From: jmwatte Date: Tue, 3 Nov 2015 13:55:08 +0100 Subject: [PATCH] renamed and updated editplugin --- beetsplug/edit.py | 357 ++++++++++++++++++++++++++++++++++++++++++ docs/plugins/edit.rst | 88 +++++++++++ 2 files changed, 445 insertions(+) create mode 100644 beetsplug/edit.py create mode 100644 docs/plugins/edit.rst diff --git a/beetsplug/edit.py b/beetsplug/edit.py new file mode 100644 index 000000000..00de0f444 --- /dev/null +++ b/beetsplug/edit.py @@ -0,0 +1,357 @@ +# This file is part of beets. +# Copyright 2015 jean-marie winters +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""open tags of items in texteditor,change them and save back to the items. +""" +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from beets import plugins +from beets.ui import Subcommand, decargs, library, print_ +import subprocess +import difflib +import yaml +import collections +import webbrowser +from sys import exit +from beets import config +from beets import ui +from tempfile import NamedTemporaryFile + + +class EditPlugin(plugins.BeetsPlugin): + + def __init__(self): + super(EditPlugin, self).__init__() + + self.config.add({ + 'style': 'yaml', + 'editor': '', + 'diff_method': '', + 'browser': '', + 'albumfields': 'album albumartist', + 'itemfields': 'track title artist album ', + 'not_fields': 'path', + 'separator': '-' + + }) + self.style = self.config['style'].get(unicode) + """the editor field in the config lets you specify your editor. + Defaults to open with webrowser module""" + self.editor = self.config['editor'].as_str_seq() + """the html_viewer field in your config lets you specify + your htmlviewer. Defaults to open with webrowser module""" + self.browser = self.config['browser'].as_str_seq() + """the diff_method field in your config picks the way to see your + changes. Options are: + 'ndiff'(2 files with differences), + 'unified'(just the different lines and a few lines of context), + 'html'(view in html-format), + 'vimdiff'(view in VIM)""" + self.diff_method = self.config['diff_method'].get(unicode) + """the albumfields field in your config sets the tags that + you want to see/change for albums. + Defaults to album albumartist. + the ID tag will always be listed as it is used to identify the item""" + self.albumfields = self.config['albumfields'].as_str_seq() + """the itemfields field in your config sets the tags that + you want to see/change or items. + Defaults to track title artist album. + the ID tag will always be listed as it is used to identify the item""" + self.itemfields = self.config['itemfields'].as_str_seq() + '''the not_fields field in your config sets the tags that + will not be changed. + If you happen to change them, they will be restored to the original + value. The ID of an item will never be changed.''' + self.not_fields = self.config['not_fields'].as_str_seq() + '''the separator in your config sets the separator that will be used + between fields in your terminal. Defaults to -''' + self.separator = self.config['separator'].get(unicode) + self.ed = None + self.ed_args = None + self.brw = None + self.brw_args = None + + def commands(self): + edit_command = Subcommand( + 'edit', + help='send items to yamleditor for editing tags') + edit_command.parser.add_option( + '-e', '--extra', + action='store', + help='add additional fields to edit', + ) + edit_command.parser.add_option( + '--all', + action='store_true', dest='all', + help='add all fields to edit', + ) + edit_command.parser.add_all_common_options() + edit_command.func = self.editor_music + return[edit_command] + + def editor_music(self, lib, opts, args): + if self.editor: + self.ed_args = self.editor[1:] if len(self.editor) > 1 else None + self.ed = self.editor[0] if self.editor else None + if self.browser: + self.brw_args = self.browser[1:] if len(self.browser) > 1 else None + self.brw = self.browser[0] if self.browser else None + + # edit tags in a textfile in yaml-style + query = decargs(args) + # makes a string representation of an object + # for now yaml but we could add html,pprint,toml + self.print_items = { + 'yaml': self.print_to_yaml} + # makes an object from a string representation + # for now yaml but we could add html,pprint,toml + self.string_to_dict = { + 'yaml': self.yaml_to_dict} + # 4 ways to view the changes in objects + self.diffresults = { + 'ndiff': self.ndiff, + 'unified': self.unified, + 'html': self.html, + 'vimdiff': self.vimdiff} + # make a dictionary from the chosen fields + # you can do em all or a selection + self.make_dict = { + 'all': self.get_all_fields, + "selected": self.get_selected_fields} + + objs = self._get_objs(lib, opts, query) + if not objs: + print_('nothing found') + return + fmt = self.get_fields_from(objs, opts) + print_(fmt) + [print_(format(item, fmt)) for item in objs] + if not ui.input_yn(ui.colorize('action_default', "Edit?(n/y)"), True): + return + dict_from_objs = self.make_dict[self.pick](self.fields, objs, opts) + newyaml, oldyaml = self.change_objs(dict_from_objs) + changed_objs = self.check_diff(newyaml, oldyaml) + if not changed_objs: + print_("nothing to change") + return + self.save_items(changed_objs, lib, fmt, opts) + + def print_to_yaml(self, arg): + # from object to yaml + return yaml.safe_dump_all( + arg, + allow_unicode=True, + default_flow_style=False) + + def yaml_to_dict(self, yam): + # from yaml to object + return yaml.load_all(yam) + + def _get_objs(self, lib, opts, query): + # get objects from a query + if opts.album: + return list(lib.albums(query)) + else: + return list(lib.items(query)) + + def get_fields_from(self, objs, opts): + # construct a list of fields we need + cl = ui.colorize('action', self.separator) + # see if we need album or item fields + self.fields = self.albumfields if opts.album else self.itemfields + # if opts.format is given only use those fields + if opts.format: + self.fields = [] + self.fields.extend((opts.format).replace('$', "").split()) + # if opts.extra is given add those + if opts.extra: + fi = (opts.extra).replace('$', "").split() + self.fields.extend([f for f in fi if f not in self.fields]) + # make sure we got the id for identification + if 'id' not in self.fields: + self.fields.insert(0, 'id') + # we need all the fields + if opts.all: + self.fields = None + self.pick = "all" + print_(ui.colorize('text_warning', "edit all fields from:")) + if opts.album: + fmt = cl + cl.join(['$albumartist', '$album']) + else: + fmt = cl + cl.join(['$title', '$artist']) + else: + for it in self.fields: + if opts.album: + if it not in library.Album.all_keys(): + print_( + "{} not in albumfields.Removed it.".format( + ui.colorize( + 'text_warning', it))) + self.fields.remove(it) + else: + if it not in library.Item.all_keys(): + print_( + "{} not in itemfields.Removed it.".format( + ui.colorize( + 'text_warning', it))) + self.fields.remove(it) + self.pick = "selected" + fmtfields = ["$" + it for it in self.fields] + fmt = cl + cl.join(fmtfields[1:]) + + return fmt + + def get_selected_fields(self, myfields, objs, opts): + a = [] + for mod in objs: + a.append([{fi: mod[fi]}for fi in myfields]) + return a + + def get_all_fields(self, myfields, objs, opts): + a = [] + for mod in objs: + a.append([{fi: mod[fi]} for fi in sorted(mod._fields)]) + return a + + def change_objs(self, dict_items): + # construct a yaml from the original object-fields + # and make a yaml that we can change in the text-editor + oldyaml = self.print_items[self.style](dict_items) + newyaml = self.print_items[self.style](dict_items) + new = NamedTemporaryFile(suffix='.yaml', delete=False) + new.write(newyaml) + new.close() + if not self.ed: + webbrowser.open(new.name, new=2, autoraise=True) + else: + callmethod = [self.ed] + if self.ed_args: + callmethod.extend(self.ed_args) + callmethod.append(new.name) + subprocess.check_call(callmethod) + + if ui.input_yn(ui.colorize('action_default', "done?(y)"), True): + with open(new.name) as f: + newyaml = f.read() + return newyaml, oldyaml + else: + exit() + + def save_items(self, oldnewlist, lib, fmt, opts): + oldset = [] + newset = [] + for old, new in oldnewlist: + oldset.append(old) + newset.append(new) + + no = [] + for newitem in range(0, len(newset)): + ordict = collections.OrderedDict() + for each in newset[newitem]: + ordict.update(each) + no.append(ordict) + + changedob = [] + for each in no: + if not opts.album: + ob = lib.get_item(each['id']) + else: + ob = lib.get_album(each['id']) + ob.update(each) + changedob.append(ob) + + if self.diff_method: + ostr = self.print_items[self.style](oldset) + nwstr = self.print_items[self.style](newset) + pprint.pprint(self.diff_method) + pprint.pprint(type(self.diff_method)) + self.diffresults[self.diff_method](ostr, nwstr) + else: + for obj in changedob: + ui.show_model_changes(obj) + self.save_write(changedob) + + def save_write(self, changedob): + if not ui.input_yn('really modify? (y/n)'): + return + + for ob in changedob: + if config['import']['write'].get(bool): + ob.try_sync() + else: + ob.store() + print("changed: {0}".format(ob)) + + return + + def check_diff(self, newyaml, oldyaml): + # get the changed objects + nl = self.string_to_dict[self.style](newyaml) + ol = self.string_to_dict[self.style](oldyaml) + return filter(None, map(self.reduce_it, ol, nl)) + + def reduce_it(self, ol, nl): + # if there is a forbidden field it resets them + if ol != nl: + for x in range(0, len(nl)): + if ol[x] != nl[x] and ol[x].keys()[0]in self.not_fields: + nl[x] = ol[x] + print_("reset forbidden field.") + if ol != nl: + return ol, nl + + def ndiff(self, newfilestr, oldfilestr): + newlines = newfilestr.splitlines() + oldlines = oldfilestr.splitlines() + diff = difflib.ndiff(newlines, oldlines) + print_('\n'.join(list(diff))) + return + + def unified(self, newfilestr, oldfilestr): + newlines = newfilestr.splitlines() + oldlines = oldfilestr.splitlines() + diff = difflib.unified_diff(newlines, oldlines, lineterm='') + print_('\n'.join(list(diff))) + return + + def html(self, newfilestr, oldfilestr): + newlines = newfilestr.splitlines() + oldlines = oldfilestr.splitlines() + diff = difflib.HtmlDiff() + pprint.pprint("here in html") + df = diff.make_file(newlines, oldlines) + ht = NamedTemporaryFile('w', suffix='.html', delete=False) + ht.write(df) + ht.flush() + hdn = ht.name + if not self.brw: + webbrowser.open(hdn, new=2, autoraise=True) + else: + callmethod = [self.brw] + if self.brw_args: + callmethod.extend(self.brw_args) + callmethod.append(hdn) + subprocess.call(callmethod) + return + + def vimdiff(self, newstringstr, oldstringstr): + + newdiff = NamedTemporaryFile(suffix='.old.yaml', delete=False) + newdiff.write(newstringstr) + newdiff.close() + olddiff = NamedTemporaryFile(suffix='.new.yaml', delete=False) + olddiff.write(oldstringstr) + olddiff.close() + subprocess.call(['vimdiff', newdiff.name, olddiff.name]) diff --git a/docs/plugins/edit.rst b/docs/plugins/edit.rst new file mode 100644 index 000000000..68ec7a056 --- /dev/null +++ b/docs/plugins/edit.rst @@ -0,0 +1,88 @@ +editPlugin +============ +The ``edit`` plugin lets you open the tags, fields from a group of items, edit them in a text-editor and save them back. +Add the ``edit`` plugin to your ``plugins:`` in your ``config.yaml``. Then +you simply put in a query like you normally do. +:: + + beet yamleditor beatles + beet yamleditor beatles -a + beet yamleditor beatles -f '$title $lyrics' + + + +You get a list of hits and then you can edit them. The ``edit`` opens your standard text-editor with a list of your hits and for each hit a bunch of fields. + +Without anything specified in your ``config.yaml`` for ``edit:`` you will see + +for items +:: + + $track-$title-$artist-$album + +and for albums +:: + + $album-$albumartist + +You can get fields from the cmdline by adding +:: + + -f '$genre $added' + +or + +:: + + -e '$year $comments' + +If you use ``-f '$field ...'`` you get *only* what you specified. + +If you use ``-e '$field ...'`` you get what you specified *extra*. + +If you add ``--all`` you get all the fields. + +After you edit the values in your text-editor - *and you may only edit the values, no deleting fields or adding fields!* - you save the file, answer with ``y`` on ``Done?`` and you get a summary of your changes. Check em, answer ``y`` or ``n`` and the changes are written to your library. + +Configuration +------------- + +Make a ``edit:`` section in your config.yaml ``(beet config -e)`` +:: + + edit: + editor: nano -w -p + diff_method: html + browser: firefox -private-window + albumfields: genre album + itemfields: track artist + not_fields: id path + separator: "<>" + +* ``editor:`` pick your own texteditor; add arguments if needed. If no``editor:`` then your system opens the file-extension. + +* ``diff_method:`` 4 choices. With no ``diff_method:`` you get the beets way of showing differences. + - ``ndiff``: you see original and the changed yamls with the changes. + - ``unified``: you see the changes with a bit of context. Simple and compact. + - ``html``: a html file that you can open in a browser. Looks nice. + - ``vimdiff``: gives you VIM with the diffs.You need VIM for this. + +* ``browser:`` + If you pick ``diff_method:html`` you can specify a viewer for it (if needed add arguments). If not, let your system open the file-extension. + +* The ``albumfields:`` and ``itemfields:`` lets you list the fields you want to change. + ``albumfields:`` gets picked if you put ``-a`` in your search query, else ``itemfields:``. For a list of fields + do the ``beet fields`` command. + +* The ``not_fields:``. Fields that you put in here will not be changed. You can see them but not change them. It always contains ``id`` and standard also the ``path``. + Don't want to mess with them. + +* The default ``separator:`` prints like: +:: + + -02-The Night Before-The Beatles-Help! + +but you can pick anything else. With "<>" it will look like: +:: + + <>02<>The Night Before<>The Beatles<>Help!