very first stab at a working individual-item importer flow

"beet import -i" now tags items instead of albums. There are many loose ends to
tie up (marked with TODOs in the source):
- What to do about applying non-track metadata to matched tracks? Currently it's
  just left in place.
- Plugin autotag candidates for tracks.
- No user querying yet.
- Non-autotagged -i import are unimplemented.
And, on top of those:
- Need to remove the action.TRACKS workflow and replace it with an option that
  lets you jump over to the individual-track interface from the album tagger.
This commit is contained in:
Adrian Sampson 2011-04-12 23:22:03 -07:00
parent 026b738718
commit 12854ad2ff
5 changed files with 57 additions and 22 deletions

View file

@ -315,6 +315,16 @@ def distance(items, info):
else:
return dist/dist_max
def apply_item_metadata(item, track_data):
"""Set an item's metadata from its matched info dictionary.
"""
item.artist = track_data['artist']
item.title = track_data['title']
item.mb_trackid = track_data['id']
if 'artist_id' in track_data:
item.mb_artistid = track_data['artist_id']
#TODO clear out other data?
def apply_metadata(items, info):
"""Set the items' metadata to match the data given in info. The
list of items must be ordered.
@ -535,6 +545,7 @@ def tag_item(item):
# candidates.extend(plugins.item_candidates(item))
# Sort by distance and return with recommendation.
log.debug('Found %i candidates.' % len(candidates))
candidates.sort()
rec = recommendation(candidates)
return candidates, rec

View file

@ -29,7 +29,7 @@ from beets.util import syspath, normpath
from beets.util.enumeration import enum
action = enum(
'SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'ALBUM',
'SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'APPLY',
name='action'
)
@ -124,7 +124,8 @@ class ImportConfig(object):
"""
_fields = ['lib', 'paths', 'resume', 'logfile', 'color', 'quiet',
'quiet_fallback', 'copy', 'write', 'art', 'delete',
'choose_match_func', 'should_resume_func', 'threaded', 'autot']
'choose_match_func', 'should_resume_func', 'threaded',
'autot', 'items']
def __init__(self, **kwargs):
for slot in self._fields:
setattr(self, slot, kwargs[slot])
@ -185,9 +186,14 @@ class ImportTask(object):
one for each item.
"""
assert len(self.items) == len(item_matches)
self.item_candidates = item_matches
self.item_matches = item_matches
self.is_album = False
def set_item_match(self, candidates, rec):
"""Set the match for a single-item task."""
assert len(self.items) == 1
self.item_matches = [(candidates, rec)]
def set_null_item_match(self):
"""For single-item tasks, mark the item as having no matches.
"""
@ -202,7 +208,7 @@ class ImportTask(object):
"""
assert not self.sentinel
assert choice != action.MANUAL # Not part of the task structure.
assert choice != action.ALBUM # Only used internally.
assert choice != action.APPLY # Only used internally.
if choice in (action.SKIP, action.ASIS, action.TRACKS):
self.choice_flag = choice
self.info = None
@ -212,7 +218,7 @@ class ImportTask(object):
info, items = choice
self.items = items # Reordered items list.
self.info = info
self.choice_flag = action.ALBUM # Implicit choice.
self.choice_flag = action.APPLY # Implicit choice.
def save_progress(self):
"""Updates the progress state to indicate that this album has
@ -226,8 +232,9 @@ class ImportTask(object):
# Logical decisions.
def should_create_album(self):
"""Should an album structure be created for these items?"""
assert self.is_album
if self.choice_flag in (action.ALBUM, action.ASIS):
if not self.is_album:
return False
elif self.choice_flag in (action.APPLY, action.ASIS):
return True
elif self.choice_flag in (action.TRACKS, action.SKIP):
return False
@ -235,7 +242,7 @@ class ImportTask(object):
assert False
def should_write_tags(self):
"""Should new info be written to the files' metadata?"""
if self.choice_flag == action.ALBUM:
if self.choice_flag == action.APPLY:
return True
elif self.choice_flag in (action.ASIS, action.TRACKS, action.SKIP):
return False
@ -248,8 +255,9 @@ class ImportTask(object):
"""When creating an album structure, should the album artist
field be inferred from the plurality of track artists?
"""
assert self.is_album
assert self.should_create_album()
if self.choice_flag == action.ALBUM:
if self.choice_flag == action.APPLY:
# Album artist comes from the info dictionary.
return False
elif self.choice_flag == action.ASIS:
@ -395,7 +403,11 @@ def apply_choices(config):
# Change metadata, move, and copy.
if task.should_write_tags():
autotag.apply_metadata(task.items, task.info)
if task.is_album:
autotag.apply_metadata(task.items, task.info)
else:
for item, info in zip(task.items, task.info):
autotag.apply_item_metadata(item, info)
if config.copy and config.delete:
old_paths = [os.path.realpath(item.path)
for item in task.items]
@ -445,7 +457,7 @@ def apply_choices(config):
task.save_progress()
# Single-item pipeline stages.
# Individual-item pipeline stages.
def read_items(config):
"""Reads individual items by recursively descending into a set of
@ -464,7 +476,7 @@ def item_lookup(config):
task = None
while True:
task = yield task
task.set_null_item_match() #TODO
task.set_item_match(*autotag.tag_item(task.items[0]))
def item_query(config):
"""A coroutine that queries the user for input on single-item
@ -473,7 +485,7 @@ def item_query(config):
task = None
while True:
task = yield task
task.set_choice(action.ASIS) # TODO
task.set_choice(action.ASIS) #TODO actually query user
# Main driver.
@ -485,13 +497,19 @@ def run_import(**kwargs):
config = ImportConfig(**kwargs)
# Set up the pipeline.
stages = [read_albums(config)]
if config.autot:
# Only look up and query the user when autotagging.
stages += [initial_lookup(config), user_query(config)]
if config.items:
# Individual item importer.
stages = [read_items(config), item_lookup(config), item_query(config)]
#TODO non-autotagged
else:
# When not autotagging, just display progress.
stages += [show_progress(config)]
# Whole-album importer.
stages = [read_albums(config)]
if config.autot:
# Only look up and query the user when autotagging.
stages += [initial_lookup(config), user_query(config)]
else:
# When not autotagging, just display progress.
stages += [show_progress(config)]
stages += [apply_choices(config)]
pl = pipeline.Pipeline(stages)

View file

@ -283,7 +283,7 @@ def choose_match(task, config):
# The import command.
def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
color, delete, quiet, resume, quiet_fallback):
color, delete, quiet, resume, quiet_fallback, items):
"""Import the files in the given list of paths, tagging each leaf
directory as an album. If copy, then the files are copied into
the library folder. If write, then new metadata is written to the
@ -332,6 +332,7 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
autot = autot,
choose_match_func = choose_match,
should_resume_func = should_resume,
items = items,
)
# If we were logging, close the file.
@ -368,6 +369,8 @@ import_cmd.parser.add_option('-q', '--quiet', action='store_true',
dest='quiet', help="never prompt for input: skip albums instead")
import_cmd.parser.add_option('-l', '--log', dest='logpath',
help='file to log untaggable albums for later review')
import_cmd.parser.add_option('-i', '--items', dest='items',
help='import individual tracks instead of full albums')
def import_func(lib, config, opts, args):
copy = opts.copy if opts.copy is not None else \
ui.config_val(config, 'beets', 'import_copy',
@ -387,6 +390,7 @@ def import_func(lib, config, opts, args):
quiet = opts.quiet if opts.quiet is not None else DEFAULT_IMPORT_QUIET
quiet_fallback_str = ui.config_val(config, 'beets', 'import_quiet_fallback',
DEFAULT_IMPORT_QUIET_FALLBACK)
items = opts.items
# Resume has three options: yes, no, and "ask" (None).
resume = opts.resume if opts.resume is not None else \
@ -404,7 +408,7 @@ def import_func(lib, config, opts, args):
else:
quiet_fallback = importer.action.SKIP
import_files(lib, args, copy, write, autot, opts.logpath, art, threaded,
color, delete, quiet, resume, quiet_fallback)
color, delete, quiet, resume, quiet_fallback, items)
import_cmd.func = import_func
default_commands.append(import_cmd)

View file

@ -79,6 +79,7 @@ def iconfig(lib, **kwargs):
should_resume_func = lambda _: False,
threaded = False,
autot = True,
items = False,
)
for k, v in kwargs.items():
setattr(config, k, v)

View file

@ -27,7 +27,7 @@ TEST_TITLES = ('The Opener','The Second Track','The Last Track')
class NonAutotaggedImportTest(unittest.TestCase):
def setUp(self):
self.io = _common.DummyIO()
#self.io.install()
self.io.install()
self.libdb = os.path.join(_common.RSRC, 'testlib.blb')
self.lib = library.Library(self.libdb)
@ -96,6 +96,7 @@ class NonAutotaggedImportTest(unittest.TestCase):
quiet_fallback='skip',
choose_match_func = None,
should_resume_func = None,
items=False,
)
return paths