Introduce a new Proposal type for tag results

tag_album and tag_item now return a Proposal. The idea is that plugin actions
should also be able to return Proposal values, just like the built-in actions.
This commit is contained in:
Adrian Sampson 2016-12-28 13:21:55 -05:00
parent 9b0a867c73
commit 3578f0d429
3 changed files with 44 additions and 25 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)