mirror of
https://github.com/beetbox/beets.git
synced 2026-01-16 21:25:14 +01:00
renamed and updated editplugin
This commit is contained in:
parent
fe012eae77
commit
2d77861e4d
2 changed files with 445 additions and 0 deletions
357
beetsplug/edit.py
Normal file
357
beetsplug/edit.py
Normal file
|
|
@ -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])
|
||||
88
docs/plugins/edit.rst
Normal file
88
docs/plugins/edit.rst
Normal file
|
|
@ -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!
|
||||
Loading…
Reference in a new issue