diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 992ff73bc..3c46b13ac 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -283,8 +283,9 @@ def tag_album(items): and a little bit more: - The list of items, possibly reordered. - The current metadata: an (artist, album) tuple. - - The inferred metadata dictionary. - - The distance between the current and new metadata. + - A list of (distance, info) tuples where info is a dictionary + containing the inferred tags. The list is sorted by + distance (i.e., best match first). May raise an AutotagError if existing metadata is insufficient. """ # Get current and candidate metadata. @@ -293,10 +294,9 @@ def tag_album(items): raise InsufficientMetadataError() candidates = mb.match_album(cur_artist, cur_album, len(items)) - best = None - best_dist = None + # Get the distance to each candidate. + dist_and_cands = [] for info in _first_n(candidates, MAX_CANDIDATES): - # Make sure the album has the correct number of tracks. if len(items) != len(info['tracks']): continue @@ -309,16 +309,13 @@ def tag_album(items): # Get the change distance. dist = distance(items, info) - # Compare this to the best. - if best_dist is None or dist < best_dist: - best_dist = dist - best = info - - # No suitable candidates. - if best is None or best_dist > GIVEUP_DIST: - #fixme Remove restriction on track numbers then requery for - # diagnosis. - raise UnknownAlbumError() + dist_and_cands.append((dist, info)) - return items, (cur_artist, cur_album), best, best_dist + if not dist_and_cands: + raise UnknownAlbumError('so feasible matches found') + + # Sort by distance. + dist_and_cands.sort() + + return items, (cur_artist, cur_album), dist_and_cands diff --git a/bts b/bts index a9414c5b9..be3e8e881 100755 --- a/bts +++ b/bts @@ -63,6 +63,60 @@ def _input_yn(prompt, require=False): return False resp = raw_input("Type 'y' or 'n': ").strip() +def choose_candidate(items, cur_artist, cur_album, candidates): + """Given current metadata and a sorted list of + (distance, candidate) pairs, ask the user for a selection + of which candidate to use. Returns the selected candidate. If no + candidate is judged good enough, returns None. + """ + # Is the change good enough? + THRESH = 0.1 #fixme + top_dist, top_info = candidates[0] + bypass_candidates = False + if top_dist <= THRESH: + dist, info = top_dist, top_info + bypass_candidates = True + + while True: + # Display and choose from candidates. + if not bypass_candidates: + print 'Candidates:' + for i, (dist, info) in enumerate(candidates): + print '%i. %s - %s (%f)' % (i+1, info['artist'], + info['album'], dist) + sel = None + while not sel: + # Ask the user for a choice. + inp = raw_input('Enter a number or "s" to skip: ') + inp = inp.strip() + if inp.lower().startswith('s'): + return None + try: + sel = int(inp) + except ValueError: + pass + if not (1 <= sel <= len(candidates)): + sel = None + dist, info = candidates[sel-1] + bypass_candidates = False + + # Show what we're about to do. + if cur_artist != info['artist'] or cur_album != info['album']: + print "Correcting tags from:" + print ' %s - %s' % (cur_artist, cur_album) + print "To:" + print ' %s - %s' % (info['artist'], info['album']) + else: + print "Tagging: %s - %s" % (info['artist'], info['album']) + print '(Distance: %f)' % dist + for item, track_data in zip(items, info['tracks']): + if item.title != track_data['title']: + print " * %s -> %s" % (item.title, track_data['title']) + + # Warn if change is significant. + if dist > 0.0: + if not _input_yn("Apply change ([y]/n)? "): + continue def tag_album(items, lib, copy=True, write=True): """Import items into lib, tagging them as an album. If copy, then @@ -71,27 +125,15 @@ def tag_album(items, lib, copy=True, write=True): """ # Infer tags. try: - items,(cur_artist,cur_album),info,dist = autotag.tag_album(items) + items,(cur_artist,cur_album),candidates = autotag.tag_album(items) except autotag.AutotagError: print "Untaggable album:", os.path.dirname(items[0].path) return - # Show what we're about to do. - if cur_artist != info['artist'] or cur_album != info['album']: - print "Correcting tags from:" - print ' %s - %s' % (cur_artist, cur_album) - print "To:" - print ' %s - %s' % (info['artist'], info['album']) - else: - print "Tagging: %s - %s" % (info['artist'], info['album']) - for item, track_data in zip(items, info['tracks']): - if item.title != track_data['title']: - print " * %s -> %s" % (item.title, track_data['title']) - - # Warn if change is significant. - if dist > 0.0: - if not _input_yn("Apply change ([y]/n)? "): - return + # Choose which tags to use. + info = choose_candidate(items, cur_artist, cur_album, candidates) + if not info: + return # Ensure that we don't have the album already. q = library.AndQuery((library.MatchQuery('artist', info['artist']),