mirror of
https://github.com/beetbox/beets.git
synced 2026-01-20 07:02:39 +01:00
manual specification of MBIDs
This commit is contained in:
parent
da6ee13159
commit
a074db78e1
5 changed files with 80 additions and 20 deletions
3
NEWS
3
NEWS
|
|
@ -5,6 +5,9 @@
|
|||
* The "list" command now accepts a "-p" switch that causes it to show
|
||||
paths instead of titles. This makes the output of "beet ls -p"
|
||||
suitable for piping into another command such as xargs.
|
||||
* The importer now provides the option to specify a MusicBrainz ID
|
||||
manually if the built-in searching isn't working for a particular
|
||||
album or track.
|
||||
* The import logger has been improved for "always-on" use. First,
|
||||
it is now possible to specify a log file in .beetsconfig. Also,
|
||||
logs are now appended rather than overwritten and contain
|
||||
|
|
|
|||
|
|
@ -386,13 +386,16 @@ def match_by_id(items):
|
|||
# Is there a consensus on the MB album ID?
|
||||
albumids = [item.mb_albumid for item in items if item.mb_albumid]
|
||||
if not albumids:
|
||||
log.debug('No album IDs found.')
|
||||
return None
|
||||
|
||||
# If all album IDs are equal, look up the album.
|
||||
if bool(reduce(lambda x,y: x if x==y else (), albumids)):
|
||||
albumid = albumids[0]
|
||||
log.debug('Searching for discovered album ID: ' + albumid)
|
||||
return mb.album_for_id(albumid)
|
||||
else:
|
||||
log.debug('No album ID consensus.')
|
||||
return None
|
||||
|
||||
#fixme In the future, at the expense of performance, we could use
|
||||
|
|
@ -456,7 +459,8 @@ def validate_candidate(items, tuple_dict, info):
|
|||
|
||||
tuple_dict[info['album_id']] = dist, ordered, info
|
||||
|
||||
def tag_album(items, timid=False, search_artist=None, search_album=None):
|
||||
def tag_album(items, timid=False, search_artist=None, search_album=None,
|
||||
search_id=None):
|
||||
"""Bundles together the functionality used to infer tags for a
|
||||
set of items comprised by an album. Returns everything relevant:
|
||||
- The current artist.
|
||||
|
|
@ -469,8 +473,8 @@ def tag_album(items, timid=False, search_artist=None, search_album=None):
|
|||
or RECOMMEND_NONE; indicating that the first candidate is
|
||||
very likely, it is somewhat likely, or no conclusion could
|
||||
be reached.
|
||||
If search_artist and search_album are provided, then they are used
|
||||
as search terms in place of the current metadata.
|
||||
If search_artist and search_album or search_id are provided, then
|
||||
they are used as search terms in place of the current metadata.
|
||||
May raise an AutotagError if existing metadata is insufficient.
|
||||
"""
|
||||
# Get current metadata.
|
||||
|
|
@ -481,17 +485,29 @@ def tag_album(items, timid=False, search_artist=None, search_album=None):
|
|||
out_tuples = {}
|
||||
|
||||
# Try to find album indicated by MusicBrainz IDs.
|
||||
id_info = match_by_id(items)
|
||||
if search_id:
|
||||
log.debug('Searching for album ID: ' + search_id)
|
||||
id_info = mb.album_for_id(search_id)
|
||||
else:
|
||||
id_info = match_by_id(items)
|
||||
if id_info:
|
||||
validate_candidate(items, out_tuples, id_info)
|
||||
rec = recommendation(out_tuples.values())
|
||||
log.debug('Album ID match recommendation is ' + str(rec))
|
||||
if out_tuples and not timid:
|
||||
# If we have a very good MBID match, return immediately.
|
||||
# Otherwise, this match will compete against metadata-based
|
||||
# matches.
|
||||
rec = recommendation(out_tuples.values())
|
||||
if rec == RECOMMEND_STRONG:
|
||||
log.debug('ID match.')
|
||||
return cur_artist, cur_album, out_tuples.values(), rec
|
||||
|
||||
# If searching by ID, don't continue to metadata search.
|
||||
if search_id is not None:
|
||||
if out_tuples:
|
||||
return cur_artist, cur_album, out_tuples.values(), rec
|
||||
else:
|
||||
return cur_artist, cur_album, [], RECOMMEND_NONE
|
||||
|
||||
# Search terms.
|
||||
if not (search_artist and search_album):
|
||||
|
|
@ -530,19 +546,21 @@ def tag_album(items, timid=False, search_artist=None, search_album=None):
|
|||
rec = recommendation(out_tuples)
|
||||
return cur_artist, cur_album, out_tuples, rec
|
||||
|
||||
def tag_item(item, timid=False, search_artist=None, search_title=None):
|
||||
def tag_item(item, timid=False, search_artist=None, search_title=None,
|
||||
search_id=None):
|
||||
"""Attempts to find metadata for a single track. Returns a
|
||||
`(candidates, recommendation)` pair where `candidates` is a list
|
||||
of `(distance, track_info)` pairs. `search_artist` and
|
||||
`search_title` may be used to override the current metadata for
|
||||
the purposes of the MusicBrainz category.
|
||||
the purposes of the MusicBrainz title; likewise `search_id`.
|
||||
"""
|
||||
candidates = []
|
||||
|
||||
# First, try matching by MusicBrainz ID.
|
||||
trackid = item.mb_trackid
|
||||
trackid = search_id or item.mb_trackid
|
||||
if trackid:
|
||||
track_info = mb.track_for_id(item.mb_trackid)
|
||||
log.debug('Searching for track ID: ' + trackid)
|
||||
track_info = mb.track_for_id(trackid)
|
||||
if track_info:
|
||||
dist = track_distance(item, track_info, incl_artist=True)
|
||||
candidates.append((dist, track_info))
|
||||
|
|
@ -551,6 +569,13 @@ def tag_item(item, timid=False, search_artist=None, search_title=None):
|
|||
if rec == RECOMMEND_STRONG and not timid:
|
||||
log.debug('Track ID match.')
|
||||
return candidates, rec
|
||||
|
||||
# If we're searching by ID, don't proceed.
|
||||
if search_id is not None:
|
||||
if candidates:
|
||||
return candidates, rec
|
||||
else:
|
||||
return [], RECOMMEND_NONE
|
||||
|
||||
# Search terms.
|
||||
if not (search_artist and search_title):
|
||||
|
|
|
|||
|
|
@ -281,7 +281,8 @@ def album_for_id(albumid):
|
|||
inc = mbws.ReleaseIncludes(artist=True, tracks=True)
|
||||
try:
|
||||
album = _query_wrap(query.getReleaseById, albumid, inc)
|
||||
except (mbws.ResourceNotFoundError, mbws.RequestError):
|
||||
except (mbws.ResourceNotFoundError, mbws.RequestError), exc:
|
||||
log.debug('Album ID match failed: ' + str(exc))
|
||||
return None
|
||||
return release_dict(album, album.tracks)
|
||||
|
||||
|
|
@ -293,6 +294,7 @@ def track_for_id(trackid):
|
|||
inc = mbws.TrackIncludes(artist=True)
|
||||
try:
|
||||
track = _query_wrap(query.getTrackById, trackid, inc)
|
||||
except (mbws.ResourceNotFoundError, mbws.RequestError):
|
||||
except (mbws.ResourceNotFoundError, mbws.RequestError), exc:
|
||||
log.debug('Track ID match failed: ' + str(exc))
|
||||
return None
|
||||
return track_dict(track)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ from beets.util import syspath, normpath
|
|||
from beets.util.enumeration import enum
|
||||
|
||||
action = enum(
|
||||
'SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'APPLY',
|
||||
'SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'APPLY', 'MANUAL_ID',
|
||||
name='action'
|
||||
)
|
||||
|
||||
|
|
@ -249,7 +249,8 @@ class ImportTask(object):
|
|||
automatically).
|
||||
"""
|
||||
assert not self.sentinel
|
||||
assert choice != action.MANUAL # Not part of the task structure.
|
||||
# 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):
|
||||
self.choice_flag = choice
|
||||
|
|
|
|||
|
|
@ -190,9 +190,11 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
if not candidates:
|
||||
print_("No match found.")
|
||||
if singleton:
|
||||
opts = ('Use as-is', 'Skip', 'Enter search', 'aBort')
|
||||
opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id',
|
||||
'aBort')
|
||||
else:
|
||||
opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search', 'aBort')
|
||||
opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search',
|
||||
'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
if sel == 'u':
|
||||
return importer.action.ASIS
|
||||
|
|
@ -205,6 +207,8 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
return importer.action.SKIP
|
||||
elif sel == 'b':
|
||||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
|
@ -238,10 +242,11 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
|
||||
# Ask the user for a choice.
|
||||
if singleton:
|
||||
opts = ('Skip', 'Use as-is', 'Enter search', 'aBort')
|
||||
opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id',
|
||||
'aBort')
|
||||
else:
|
||||
opts = ('Skip', 'Use as-is', 'as Tracks', 'Enter search',
|
||||
'aBort')
|
||||
'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, numrange=(1, len(candidates)),
|
||||
color=color)
|
||||
if sel == 's':
|
||||
|
|
@ -255,6 +260,8 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
return importer.action.TRACKS
|
||||
elif sel == 'b':
|
||||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
else: # Numerical selection.
|
||||
if singleton:
|
||||
dist, info = candidates[sel-1]
|
||||
|
|
@ -278,10 +285,10 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
# Ask for confirmation.
|
||||
if singleton:
|
||||
opts = ('Apply', 'More candidates', 'Skip', 'Use as-is',
|
||||
'Enter search', 'aBort')
|
||||
'Enter search', 'enter Id', 'aBort')
|
||||
else:
|
||||
opts = ('Apply', 'More candidates', 'Skip', 'Use as-is',
|
||||
'as Tracks', 'Enter search', 'aBort')
|
||||
'as Tracks', 'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
if sel == 'a':
|
||||
if singleton:
|
||||
|
|
@ -301,6 +308,8 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
return importer.action.MANUAL
|
||||
elif sel == 'b':
|
||||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
|
||||
def manual_search(singleton):
|
||||
"""Input either an artist and album (for full albums) or artist and
|
||||
|
|
@ -311,6 +320,12 @@ def manual_search(singleton):
|
|||
.decode(sys.stdin.encoding)
|
||||
return artist.strip(), name.strip()
|
||||
|
||||
def manual_id(singleton):
|
||||
"""Input a MusicBrainz ID, either for an album or a track.
|
||||
"""
|
||||
prompt = 'Enter MusicBrainz %s ID: ' % ('track' if singleton else 'album')
|
||||
return raw_input(prompt).decode(sys.stdin.encoding).strip()
|
||||
|
||||
def choose_match(task, config):
|
||||
"""Given an initial autotagging of items, go through an interactive
|
||||
dance with the user to ask for a choice of metadata. Returns an
|
||||
|
|
@ -352,6 +367,15 @@ def choose_match(task, config):
|
|||
search_album)
|
||||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
elif choice is importer.action.MANUAL_ID:
|
||||
# Try a manually-entered ID.
|
||||
search_id = manual_id(False)
|
||||
try:
|
||||
_, _, candidates, rec = \
|
||||
autotag.tag_album(task.items, config.timid,
|
||||
search_id=search_id)
|
||||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
else:
|
||||
# We have a candidate! Finish tagging. Here, choice is
|
||||
# an (info, items) pair as desired.
|
||||
|
|
@ -386,9 +410,14 @@ def choose_item(task, config):
|
|||
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(False)
|
||||
search_artist, search_title = manual_search(True)
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
search_artist, search_title)
|
||||
elif choice == importer.action.MANUAL_ID:
|
||||
# Ask for a track ID.
|
||||
search_id = manual_id(True)
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
search_id=search_id)
|
||||
else:
|
||||
# Chose a candidate.
|
||||
assert not isinstance(choice, importer.action)
|
||||
|
|
|
|||
Loading…
Reference in a new issue