mirror of
https://github.com/beetbox/beets.git
synced 2026-01-30 20:13:37 +01:00
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:
parent
026b738718
commit
12854ad2ff
5 changed files with 57 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue