Refactor search options to ordinary choices

`PromptChoice`s can now return `Proposal` values, which makes these two
options just "normal" actions that could be provided by plugins.
This commit is contained in:
Adrian Sampson 2016-12-28 14:08:09 -05:00
parent 6c825970ac
commit 2f6538aee2
4 changed files with 33 additions and 66 deletions

View file

@ -23,7 +23,7 @@ from beets import config
# Parts of external interface.
from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa
from .match import tag_item, tag_album # noqa
from .match import tag_item, tag_album, Proposal # noqa
from .match import Recommendation # noqa
# Global logger.

View file

@ -43,8 +43,7 @@ from enum import Enum
from beets import mediafile
action = Enum('action',
['SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'APPLY', 'MANUAL_ID',
'ALBUMS', 'RETAG'])
['SKIP', 'ASIS', 'TRACKS', 'APPLY', 'ALBUMS', 'RETAG'])
# The RETAG action represents "don't apply any match, but do record
# new metadata". It's not reachable via the standard command prompt but
# can be used by plugins.
@ -443,7 +442,6 @@ class ImportTask(BaseImportTask):
indicates that an action has been selected for this task.
"""
# Not part of the task structure:
assert choice not in (action.MANUAL, action.MANUAL_ID)
assert choice != action.APPLY # Only used internally.
if choice in (action.SKIP, action.ASIS, action.TRACKS, action.ALBUMS,
action.RETAG):

View file

@ -509,7 +509,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
to the user.
Returns one of the following:
* the result of the choice, which may be SKIP, ASIS, TRACKS, or MANUAL
* the result of the choice, which may be SKIP, ASIS, or TRACKS
* a candidate (an AlbumMatch/TrackMatch object)
* a chosen `PromptChoice` from `extra_choices`
"""
@ -536,21 +536,17 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
print_(u'For help, see: '
u'http://beets.readthedocs.org/en/latest/faq.html#nomatch')
opts = (u'Use as-is', u'as Tracks', u'Group albums', u'Skip',
u'Enter search', u'enter Id', u'aBort')
u'aBort')
sel = ui.input_options(opts + extra_opts)
if sel == u'u':
return importer.action.ASIS
elif sel == u't':
assert not singleton
return importer.action.TRACKS
elif sel == u'e':
return importer.action.MANUAL
elif sel == u's':
return importer.action.SKIP
elif sel == u'b':
raise importer.ImportAbort()
elif sel == u'i':
return importer.action.MANUAL_ID
elif sel == u'g':
return importer.action.ALBUMS
elif sel in extra_actions:
@ -603,11 +599,10 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
# Ask the user for a choice.
if singleton:
opts = (u'Skip', u'Use as-is', u'Enter search', u'enter Id',
u'aBort')
opts = (u'Skip', u'Use as-is', u'aBort')
else:
opts = (u'Skip', u'Use as-is', u'as Tracks', u'Group albums',
u'Enter search', u'enter Id', u'aBort')
u'aBort')
sel = ui.input_options(opts + extra_opts,
numrange=(1, len(candidates)))
if sel == u's':
@ -616,15 +611,11 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
return importer.action.ASIS
elif sel == u'm':
pass
elif sel == u'e':
return importer.action.MANUAL
elif sel == u't':
assert not singleton
return importer.action.TRACKS
elif sel == u'b':
raise importer.ImportAbort()
elif sel == u'i':
return importer.action.MANUAL_ID
elif sel == u'g':
return importer.action.ALBUMS
elif sel in extra_actions:
@ -650,11 +641,10 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
# Ask for confirmation.
if singleton:
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'Enter search', u'enter Id', u'aBort')
u'aBort')
else:
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'as Tracks', u'Group albums', u'Enter search',
u'enter Id', u'aBort')
u'as Tracks', u'Group albums', u'aBort')
default = config['import']['default_action'].as_choice({
u'apply': u'a',
u'skip': u's',
@ -676,17 +666,13 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
elif sel == u't':
assert not singleton
return importer.action.TRACKS
elif sel == u'e':
return importer.action.MANUAL
elif sel == u'b':
raise importer.ImportAbort()
elif sel == u'i':
return importer.action.MANUAL_ID
elif sel in extra_actions:
return extra_actions[sel]
def manual_search(task):
def manual_search(session, task):
"""Get a new `Proposal` using manual search criteria.
Input either an artist and album (for full albums) or artist and
@ -704,7 +690,7 @@ def manual_search(task):
return autotag.tag_item(task.item, artist, name)
def manual_id(task):
def manual_id(session, task):
"""Get a new `Proposal` using a manually-entered ID.
Input an ID, either for an album ("release") or a track ("recording").
@ -747,8 +733,12 @@ class TerminalImportSession(importer.ImportSession):
# Loop until we have a choice.
candidates, rec = task.candidates, task.rec
while True:
# Gather additional menu choices from plugins.
extra_choices = self._get_plugin_choices(task)
# Set up menu choices.
choices = [
PromptChoice(u'e', u'Enter search', manual_search),
PromptChoice(u'i', u'enter Id', manual_id),
]
choices += self._get_plugin_choices(task)
# Ask for a choice from the user. The result of
# `choose_candidate` may be an `importer.action`, an
@ -756,7 +746,7 @@ class TerminalImportSession(importer.ImportSession):
# `PromptChoice`.
choice = choose_candidate(
candidates, False, rec, task.cur_artist, task.cur_album,
itemcount=len(task.items), extra_choices=extra_choices
itemcount=len(task.items), extra_choices=choices
)
# Basic choices that require no more action here.
@ -765,28 +755,16 @@ class TerminalImportSession(importer.ImportSession):
# Pass selection to main control flow.
return choice
# Manual search. We ask for the search terms and run the
# loop again.
elif choice is importer.action.MANUAL:
# Try again with manual search terms.
prop = manual_search(task)
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.
prop = manual_id(task)
candidates = prop.candidates
rec = prop.recommendation
# Plugin-provided choices. We invoke the associated callback
# function.
elif choice in extra_choices:
elif choice in choices:
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.action):
# MANUAL and MANUAL_ID have no effect, even if returned.
return post_choice
elif isinstance(post_choice, autotag.Proposal):
# Use the new candidates and continue around the loop.
candidates = post_choice.candidates
rec = post_choice.recommendation
# Otherwise, we have a specific match selection.
else:
@ -813,11 +791,15 @@ class TerminalImportSession(importer.ImportSession):
return action
while True:
extra_choices = self._get_plugin_choices(task)
choices = [
PromptChoice(u'e', u'Enter search', manual_search),
PromptChoice(u'i', u'enter Id', manual_id),
]
choices += self._get_plugin_choices(task)
# Ask for a choice.
choice = choose_candidate(candidates, True, rec, item=task.item,
extra_choices=extra_choices)
extra_choices=choices)
if choice in (importer.action.SKIP, importer.action.ASIS):
return choice
@ -825,23 +807,13 @@ class TerminalImportSession(importer.ImportSession):
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.
prop = manual_search(task)
candidates = prop.candidates
rec = prop.recommendation
elif choice == importer.action.MANUAL_ID:
# Ask for a track ID.
prop = manual_id(task)
candidates = prop.candidates
rec = prop.recommendation
elif choice in extra_choices:
elif choice in choices:
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.action):
# MANUAL and MANUAL_ID have no effect, even if returned.
return post_choice
elif isinstance(post_choice, autotag.Proposal):
candidates = post_choice.candidates
rec = post_choice.recommendation
else:
# Chose a candidate.

View file

@ -593,7 +593,4 @@ by the choices on the core importer prompt, and hence should not be used:
Additionally, the callback function can optionally specify the next action to
be performed by returning one of the values from ``importer.action``, which
will be passed to the main loop upon the callback has been processed. Note that
``action.MANUAL`` and ``action.MANUAL_ID`` will have no effect even if
returned by the callback, due to the current architecture of the import
process.
will be passed to the main loop upon the callback has been processed.