diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 493fd20c9..71d80e821 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -22,6 +22,7 @@ from __future__ import division, absolute_import, print_function import datetime import re from munkres import Munkres +from collections import namedtuple from beets import logging from beets import plugins @@ -52,6 +53,13 @@ class Recommendation(OrderedEnum): strong = 3 +# A structure for holding a set of possible matches to choose between. This +# consists of a list of possible candidates (i.e., AlbumInfo or TrackInfo +# objects) and a recommendation value. + +Proposal = namedtuple('Proposal', ('candidates', 'recommendation')) + + # Primary matching functionality. def current_metadata(items): @@ -379,9 +387,8 @@ def _add_candidate(items, results, info): def tag_album(items, search_artist=None, search_album=None, search_ids=[]): - """Return a tuple of a artist name, an album name, a list of - `AlbumMatch` candidates from the metadata backend, and a - `Recommendation`. + """Return a tuple of the current artist name, the current album + name, and a `Proposal` containing `AlbumMatch` candidates. The artist and album are the most common values of these fields among `items`. @@ -429,7 +436,7 @@ def tag_album(items, search_artist=None, search_album=None, if rec == Recommendation.strong: log.debug(u'ID match.') return cur_artist, cur_album, \ - list(candidates.values()), rec + Proposal(list(candidates.values()), rec) # Search terms. if not (search_artist and search_album): @@ -454,14 +461,15 @@ def tag_album(items, search_artist=None, search_album=None, # Sort and get the recommendation. candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) - return cur_artist, cur_album, candidates, rec + return cur_artist, cur_album, Proposal(candidates, rec) def tag_item(item, search_artist=None, search_title=None, search_ids=[]): - """Attempts to find metadata for a single track. Returns a - `(candidates, recommendation)` pair where `candidates` is a list of - TrackMatch objects. `search_artist` and `search_title` may be used + """Find metadata for a single track. Return a `Proposal` consisting + of `TrackMatch` objects. + + `search_artist` and `search_title` may be used to override the current metadata for the purposes of the MusicBrainz title. `search_ids` may be used for restricting the search to a list of metadata backend IDs. @@ -484,14 +492,14 @@ def tag_item(item, search_artist=None, search_title=None, if rec == Recommendation.strong and \ not config['import']['timid']: log.debug(u'Track ID match.') - return _sort_candidates(candidates.values()), rec + return Proposal(_sort_candidates(candidates.values()), rec) # If we're searching by ID, don't proceed. if search_ids: if candidates: - return _sort_candidates(candidates.values()), rec + return Proposal(_sort_candidates(candidates.values()), rec) else: - return [], Recommendation.none + return Proposal([], Recommendation.none) # Search terms. if not (search_artist and search_title): @@ -507,4 +515,4 @@ def tag_item(item, search_artist=None, search_title=None, log.debug(u'Found {0} candidates.', len(candidates)) candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) - return candidates, rec + return Proposal(candidates, rec) diff --git a/beets/importer.py b/beets/importer.py index 2c1d07c5c..3daf90cdc 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -587,12 +587,12 @@ class ImportTask(BaseImportTask): candidate IDs are stored in self.search_ids: if present, the initial lookup is restricted to only those IDs. """ - artist, album, candidates, recommendation = \ + artist, album, prop = \ autotag.tag_album(self.items, search_ids=self.search_ids) self.cur_artist = artist self.cur_album = album - self.candidates = candidates - self.rec = recommendation + self.candidates = prop.candidates + self.rec = prop.recommendation def find_duplicates(self, lib): """Return a list of albums from `lib` with the same artist and @@ -830,10 +830,9 @@ class SingletonImportTask(ImportTask): plugins.send('item_imported', lib=lib, item=item) def lookup_candidates(self): - candidates, recommendation = autotag.tag_item( - self.item, search_ids=self.search_ids) - self.candidates = candidates - self.rec = recommendation + prop = autotag.tag_item(self.item, search_ids=self.search_ids) + self.candidates = prop.candidates + self.rec = prop.recommendation def find_duplicates(self, lib): """Return a list of items from `lib` that have the same artist diff --git a/beets/ui/commands.py b/beets/ui/commands.py index b9a3e0b91..1a42561fd 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -752,18 +752,22 @@ class TerminalImportSession(importer.ImportSession): elif choice is importer.action.MANUAL: # Try again with manual search terms. search_artist, search_album = manual_search(False) - _, _, candidates, rec = autotag.tag_album( + _, _, prop = autotag.tag_album( task.items, search_artist, search_album ) + candidates = prop.candidates + rec = prop.recommendation # Manual ID. We prompt for the ID and run the loop again. elif choice is importer.action.MANUAL_ID: # Try a manually-entered ID. search_id = manual_id(False) if search_id: - _, _, candidates, rec = autotag.tag_album( + _, _, prop = autotag.tag_album( task.items, search_ids=search_id.split() ) + candidates = prop.candidates + rec = prop.recommendation # Plugin-provided choices. We invoke the associated callback # function. @@ -807,25 +811,33 @@ class TerminalImportSession(importer.ImportSession): if choice in (importer.action.SKIP, importer.action.ASIS): return choice + elif choice == importer.action.TRACKS: assert False # TRACKS is only legal for albums. + elif choice == importer.action.MANUAL: # Continue in the loop with a new set of candidates. search_artist, search_title = manual_search(True) - candidates, rec = autotag.tag_item(task.item, search_artist, - search_title) + prop = autotag.tag_item(task.item, search_artist, search_title) + candidates = prop.candidates + rec = prop.recommendation + elif choice == importer.action.MANUAL_ID: # Ask for a track ID. search_id = manual_id(True) if search_id: - candidates, rec = autotag.tag_item( - task.item, search_ids=search_id.split()) + prop = autotag.tag_item(task.item, + search_ids=search_id.split()) + candidates = prop.candidates + rec = prop.recommendation + elif choice in list(extra_ops.keys()): # Allow extra ops to automatically set the post-choice. post_choice = extra_ops[choice](self, task) if isinstance(post_choice, importer.action): # MANUAL and MANUAL_ID have no effect, even if returned. return post_choice + else: # Chose a candidate. assert isinstance(choice, autotag.TrackMatch)