first attempt at command-line modification command (#56)

This commit is contained in:
Adrian Sampson 2011-08-02 13:59:33 -07:00
parent 7220f1a852
commit a367b2764d
3 changed files with 204 additions and 14 deletions

View file

@ -726,8 +726,11 @@ class Library(BaseLibrary):
art_filename='cover',
item_fields=ITEM_FIELDS,
album_fields=ALBUM_FIELDS):
self.path = bytestring_path(path)
self.directory = bytestring_path(directory)
if path == ':memory:':
self.path = path
else:
self.path = bytestring_path(normpath(path))
self.directory = bytestring_path(normpath(directory))
if path_formats is None:
path_formats = {'default': '$artist/$album/$track $title'}
elif isinstance(path_formats, basestring):

View file

@ -20,6 +20,7 @@ import logging
import sys
import os
import time
import itertools
from beets import ui
from beets.ui import print_, decargs
@ -27,7 +28,8 @@ from beets import autotag
import beets.autotag.art
from beets import plugins
from beets import importer
from beets.util import syspath, normpath
from beets.util import syspath, normpath, ancestry
from beets import library
# Global logger.
log = logging.getLogger('beets')
@ -36,6 +38,32 @@ log = logging.getLogger('beets')
# objects that can be fed to a SubcommandsOptionParser.
default_commands = []
# Utility.
def _do_query(lib, query, album, also_items=True):
"""For commands that operate on matched items, performs a query
and returns a list of matching items and a list of matching
albums. (The latter is only nonempty when album is True.) Raises
a UserError if no items match. also_items controls whether, when
fetching albums, the associated items should be fetched also.
"""
if album:
albums = list(lib.albums(query))
items = []
if also_items:
for al in albums:
items += al.items()
else:
albums = []
items = list(lib.items(query))
if album and not albums:
raise ui.UserError('No matching albums found.')
elif not album and not items:
raise ui.UserError('No matching items found.')
return items, albums
# import: Autotagger and importer.
@ -634,17 +662,7 @@ def remove_items(lib, query, album, delete=False):
remove whole albums. If delete, also remove files from disk.
"""
# Get the matching items.
if album:
albums = list(lib.albums(query))
items = []
for al in albums:
items += al.items()
else:
items = list(lib.items(query))
if not items:
print_('No matching items found.')
return
items, albums = _do_query(lib, query, album)
# Show all the items.
for item in items:
@ -740,3 +758,93 @@ version_cmd = ui.Subcommand('version',
help='output version information')
version_cmd.func = show_version
default_commands.append(version_cmd)
# modify: Declaratively change metadata.
def modify_items(lib, mods, query, write, move, album, color):
"""Modifies matching items according to key=value assignments."""
# Parse key=value specifications into a dictionary.
allowed_keys = library.ALBUM_KEYS if album else library.ITEM_KEYS_WRITABLE
fsets = {}
for mod in mods:
key, value = mod.split('=', 1)
if key not in allowed_keys:
raise ui.UserError('"%s" is not a valid field' % key)
fsets[key] = value
# Get the items to modify.
items, albums = _do_query(lib, query, album, False)
objs = albums if album else items
# Preview change.
print_('Modifying %i %ss.' % (len(objs), 'album' if album else 'item'))
for obj in objs:
# Identify the changed object.
if album:
print_(u'* %s - %s' % (obj.albumartist, obj.album))
else:
print_(u'* %s - %s' % (obj.artist, obj.title))
# Show each change.
for field, value in fsets.iteritems():
curval = getattr(obj, field)
if curval != value:
if color:
curval, value = ui.colordiff(curval, value)
print_(u' %s: %s -> %s' % (field, curval, value))
# Confirm.
extra = ' and write tags' if write else ''
if not ui.input_yn('Really modify%s (Y/n)?' % extra):
return
# Apply changes to database.
for obj in objs:
for field, value in fsets.iteritems():
setattr(obj, field, value)
if move:
cur_path = obj.item_dir() if album else obj.path
if lib.directory in ancestry(cur_path): # In library?
log.debug('moving object %s' % cur_path)
if album:
obj.move()
else:
obj.move(lib)
# When modifying items, we have to store them to the database.
if not album:
lib.store(obj)
lib.save()
# Apply tags if requested.
if write:
if album:
items = itertools.chain(*(a.items() for a in albums))
for item in items:
item.write()
modify_cmd = ui.Subcommand('modify',
help='change metadata fields', aliases=('mod',))
modify_cmd.parser.add_option('-M', '--nomove', action='store_false',
default=True, dest='move', help="don't move files in library")
modify_cmd.parser.add_option('-w', '--write', action='store_true',
default=None, help="write new metadata to files' tags (default)")
modify_cmd.parser.add_option('-W', '--nowrite', action='store_false',
dest='write', help="don't write metadata (opposite of -w)")
modify_cmd.parser.add_option('-a', '--album', action='store_true',
help='modify whole albums instead of tracks')
def modify_func(lib, config, opts, args):
args = decargs(args)
mods = [a for a in args if '=' in a]
query = [a for a in args if '=' not in a]
if not mods:
raise ui.UserError('no modifications specified')
write = opts.write if opts.write is not None else \
ui.config_val(config, 'beets', 'import_write',
DEFAULT_IMPORT_WRITE, bool)
color = ui.config_val(config, 'beets', 'color', DEFAULT_COLOR, bool)
modify_items(lib, mods, query, write, opts.move, opts.album, color)
modify_cmd.func = modify_func
default_commands.append(modify_cmd)

View file

@ -119,6 +119,85 @@ class RemoveTest(unittest.TestCase):
self.assertEqual(len(list(items)), 0)
self.assertFalse(os.path.exists(self.i.path))
class ModifyTest(unittest.TestCase):
def setUp(self):
self.io = _common.DummyIO()
self.io.install()
self.libdir = os.path.join(_common.RSRC, 'testlibdir')
os.mkdir(self.libdir)
# Copy a file into the library.
self.lib = library.Library(':memory:', self.libdir)
self.i = library.Item.from_path(os.path.join(_common.RSRC, 'full.mp3'))
self.lib.add(self.i, True)
self.album = self.lib.add_album([self.i])
def tearDown(self):
self.io.restore()
shutil.rmtree(self.libdir)
def _modify(self, mods, query=(), write=False, move=False, album=False):
self.io.addinput('y')
commands.modify_items(self.lib, mods, query,
write, move, album, True)
def test_modify_item_dbdata(self):
self._modify(["title=newTitle"])
item = self.lib.items().next()
self.assertEqual(item.title, 'newTitle')
def test_modify_album_dbdata(self):
self._modify(["album=newAlbum"], album=True)
album = self.lib.albums()[0]
self.assertEqual(album.album, 'newAlbum')
def test_modify_item_tag_unmodified(self):
self._modify(["title=newTitle"], write=False)
item = self.lib.items().next()
item.read()
self.assertEqual(item.title, 'full')
def test_modify_album_tag_unmodified(self):
self._modify(["album=newAlbum"], write=False, album=True)
item = self.lib.items().next()
item.read()
self.assertEqual(item.album, 'the album')
def test_modify_item_tag(self):
self._modify(["title=newTitle"], write=True)
item = self.lib.items().next()
item.read()
self.assertEqual(item.title, 'newTitle')
def test_modify_album_tag(self):
self._modify(["album=newAlbum"], write=True, album=True)
item = self.lib.items().next()
item.read()
self.assertEqual(item.album, 'newAlbum')
def test_item_move(self):
self._modify(["title=newTitle"], move=True)
item = self.lib.items().next()
self.assertTrue('newTitle' in item.path)
def test_album_move(self):
self._modify(["album=newAlbum"], move=True, album=True)
item = self.lib.items().next()
item.read()
self.assertTrue('newAlbum' in item.path)
def test_item_not_move(self):
self._modify(["title=newTitle"], move=False)
item = self.lib.items().next()
self.assertFalse('newTitle' in item.path)
def test_album_not_move(self):
self._modify(["album=newAlbum"], move=False, album=True)
item = self.lib.items().next()
item.read()
self.assertFalse('newAlbum' in item.path)
class PrintTest(unittest.TestCase):
def setUp(self):
self.io = _common.DummyIO()