mirror of
https://github.com/beetbox/beets.git
synced 2026-02-11 09:54:31 +01:00
use AlbumMatch/TrackMatch objects everywhere
This allows matches to indicate both missing and unmatched tracks in their candidates and solves some of the spaghetti tuples that were passed around during autotagging.
This commit is contained in:
parent
7464b138cf
commit
ce166004cb
8 changed files with 252 additions and 257 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2011, Adrian Sampson.
|
||||
# Copyright 2012, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
|
@ -22,7 +22,7 @@ from beets import library, mediafile
|
|||
from beets.util import sorted_walk, ancestry
|
||||
|
||||
# Parts of external interface.
|
||||
from .hooks import AlbumInfo, TrackInfo
|
||||
from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch
|
||||
from .match import AutotagError
|
||||
from .match import tag_item, tag_album
|
||||
from .match import RECOMMEND_STRONG, RECOMMEND_MEDIUM, RECOMMEND_NONE
|
||||
|
|
@ -115,12 +115,12 @@ def apply_item_metadata(item, track_info):
|
|||
# At the moment, the other metadata is left intact (including album
|
||||
# and track number). Perhaps these should be emptied?
|
||||
|
||||
def apply_metadata(items, album_info, per_disc_numbering=False):
|
||||
"""Set the items' metadata to match an AlbumInfo object. The list of
|
||||
items must be ordered. If `per_disc_numbering`, then the track
|
||||
numbers are per-disc instead of per-release.
|
||||
def apply_metadata(album_info, mapping, per_disc_numbering=False):
|
||||
"""Set the items' metadata to match an AlbumInfo object using a
|
||||
mapping from Items to TrackInfo objects. If `per_disc_numbering`,
|
||||
then the track numbers are per-disc instead of per-release.
|
||||
"""
|
||||
for item, track_info in zip(items, album_info.tracks):
|
||||
for item, track_info in mapping.iteritems():
|
||||
# Album, artist, track count.
|
||||
if not item:
|
||||
continue
|
||||
|
|
@ -130,7 +130,7 @@ def apply_metadata(items, album_info, per_disc_numbering=False):
|
|||
item.artist = album_info.artist
|
||||
item.albumartist = album_info.artist
|
||||
item.album = album_info.album
|
||||
item.tracktotal = len(items)
|
||||
item.tracktotal = len(album_info.tracks)
|
||||
|
||||
# Artist sort and credit names.
|
||||
item.artist_sort = track_info.artist_sort or album_info.artist_sort
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2011, Adrian Sampson.
|
||||
# Copyright 2012, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
|
@ -35,6 +35,8 @@ ALBUM_WEIGHT = 3.0
|
|||
TRACK_WEIGHT = 1.0
|
||||
# The weight of a missing track.
|
||||
MISSING_WEIGHT = 0.9
|
||||
# The weight of an extra (umatched) track.
|
||||
UNMATCHED_WEIGHT = 0.6
|
||||
# These distances are components of the track distance (that is, they
|
||||
# compete against each other but not ARTIST_WEIGHT and ALBUM_WEIGHT;
|
||||
# the overall TRACK_WEIGHT does that).
|
||||
|
|
@ -248,9 +250,14 @@ def track_distance(item, track_info, incl_artist=False):
|
|||
|
||||
return dist / dist_max
|
||||
|
||||
def distance(items, album_info):
|
||||
def distance(items, album_info, mapping):
|
||||
"""Determines how "significant" an album metadata change would be.
|
||||
Returns a float in [0.0,1.0]. The list of items must be ordered.
|
||||
Returns a float in [0.0,1.0]. `album_info` is an AlbumInfo object
|
||||
reflecting the album to be compared. `items` is a sequence of all
|
||||
Item objects that will be matched (order is not important).
|
||||
`mapping` is a dictionary mapping Items to TrackInfo objects; the
|
||||
keys are a subset of `items` and the values are a subset of
|
||||
`album_info.tracks`.
|
||||
"""
|
||||
cur_artist, cur_album, _ = current_metadata(items)
|
||||
cur_artist = cur_artist or ''
|
||||
|
|
@ -268,15 +275,18 @@ def distance(items, album_info):
|
|||
dist += string_dist(cur_album, album_info.album) * ALBUM_WEIGHT
|
||||
dist_max += ALBUM_WEIGHT
|
||||
|
||||
# Track distances.
|
||||
for i, (item, track_info) in enumerate(zip(items, album_info.tracks)):
|
||||
if item:
|
||||
dist += track_distance(item, track_info, album_info.va) * \
|
||||
TRACK_WEIGHT
|
||||
dist_max += TRACK_WEIGHT
|
||||
else:
|
||||
dist += MISSING_WEIGHT
|
||||
dist_max += MISSING_WEIGHT
|
||||
# Matched track distances.
|
||||
for item, track in mapping.iteritems():
|
||||
dist += track_distance(item, track, album_info.va) * TRACK_WEIGHT
|
||||
dist_max += TRACK_WEIGHT
|
||||
|
||||
# Extra and unmatched tracks.
|
||||
for track in set(album_info.tracks) - set(mapping.values()):
|
||||
dist += MISSING_WEIGHT
|
||||
dist_max += MISSING_WEIGHT
|
||||
for item in set(items) - set(mapping.keys()):
|
||||
dist += UNMATCHED_WEIGHT
|
||||
dist_max += UNMATCHED_WEIGHT
|
||||
|
||||
# Plugin distances.
|
||||
plugin_d, plugin_dm = plugins.album_distance(items, album_info)
|
||||
|
|
@ -287,11 +297,12 @@ def distance(items, album_info):
|
|||
if dist_max == 0.0:
|
||||
return 0.0
|
||||
else:
|
||||
return dist/dist_max
|
||||
return dist / dist_max
|
||||
|
||||
def match_by_id(items):
|
||||
"""If the items are tagged with a MusicBrainz album ID, returns an
|
||||
info dict for the corresponding album. Otherwise, returns None.
|
||||
AlbumInfo object for the corresponding album. Otherwise, returns
|
||||
None.
|
||||
"""
|
||||
# Is there a consensus on the MB album ID?
|
||||
albumids = [item.mb_albumid for item in items if item.mb_albumid]
|
||||
|
|
@ -313,15 +324,15 @@ def match_by_id(items):
|
|||
# present, but that event seems very unlikely.
|
||||
|
||||
def recommendation(results):
|
||||
"""Given a sorted list of result tuples, returns a recommendation
|
||||
flag (RECOMMEND_STRONG, RECOMMEND_MEDIUM, RECOMMEND_NONE) based
|
||||
on the results' distances.
|
||||
"""Given a sorted list of AlbumMatch or TrackMatch objects, return a
|
||||
recommendation flag (RECOMMEND_STRONG, RECOMMEND_MEDIUM,
|
||||
RECOMMEND_NONE) based on the results' distances.
|
||||
"""
|
||||
if not results:
|
||||
# No candidates: no recommendation.
|
||||
rec = RECOMMEND_NONE
|
||||
else:
|
||||
min_dist = results[0][0]
|
||||
min_dist = results[0].distance
|
||||
if min_dist < STRONG_REC_THRESH:
|
||||
# Strong recommendation level.
|
||||
rec = RECOMMEND_STRONG
|
||||
|
|
@ -331,7 +342,7 @@ def recommendation(results):
|
|||
elif min_dist <= MEDIUM_REC_THRESH:
|
||||
# Medium recommendation level.
|
||||
rec = RECOMMEND_MEDIUM
|
||||
elif results[1][0] - min_dist >= REC_GAP_THRESH:
|
||||
elif results[1].distance - min_dist >= REC_GAP_THRESH:
|
||||
# Gap between first two candidates is large.
|
||||
rec = RECOMMEND_MEDIUM
|
||||
else:
|
||||
|
|
@ -339,11 +350,11 @@ def recommendation(results):
|
|||
rec = RECOMMEND_NONE
|
||||
return rec
|
||||
|
||||
def validate_candidate(items, results, info):
|
||||
def _add_candidate(items, results, info):
|
||||
"""Given a candidate AlbumInfo object, attempt to add the candidate
|
||||
to the output dictionary of result tuples. This involves checking
|
||||
the track count, ordering the items, checking for duplicates, and
|
||||
calculating the distance.
|
||||
to the output dictionary of AlbumMatch objects. This involves
|
||||
checking the track count, ordering the items, checking for
|
||||
duplicates, and calculating the distance.
|
||||
"""
|
||||
log.debug('Candidate: %s - %s' % (info.artist, info.album))
|
||||
|
||||
|
|
@ -352,24 +363,15 @@ def validate_candidate(items, results, info):
|
|||
log.debug('Duplicate.')
|
||||
return
|
||||
|
||||
# Make sure the album has the correct number of tracks.
|
||||
if len(items) > len(info.tracks):
|
||||
log.debug('Too many items to match: %i > %i.' %
|
||||
(len(items), len(info.tracks)))
|
||||
return
|
||||
|
||||
# Put items in order.
|
||||
# Find mapping between the items and the track info.
|
||||
mapping, extra_items, extra_tracks = assign_items(items, info.tracks)
|
||||
# TEMPORARY: make ordered item list with gaps.
|
||||
ordered = [None] * len(info.tracks)
|
||||
for item, track_info in mapping.iteritems():
|
||||
ordered[track_info.index - 1] = item
|
||||
|
||||
# Get the change distance.
|
||||
dist = distance(ordered, info)
|
||||
dist = distance(items, info, mapping)
|
||||
log.debug('Success. Distance: %f' % dist)
|
||||
|
||||
results[info.album_id] = dist, ordered, info
|
||||
results[info.album_id] = hooks.AlbumMatch(dist, info, mapping,
|
||||
extra_items, extra_tracks)
|
||||
|
||||
def tag_album(items, timid=False, search_artist=None, search_album=None,
|
||||
search_id=None):
|
||||
|
|
@ -377,10 +379,8 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
set of items comprised by an album. Returns everything relevant:
|
||||
- The current artist.
|
||||
- The current album.
|
||||
- A list of (distance, items, info) tuples where info is a
|
||||
dictionary containing the inferred tags and items is a
|
||||
reordered version of the input items list. The candidates are
|
||||
sorted by distance (i.e., best match first).
|
||||
- A list of AlbumMatch objects. The candidates are sorted by
|
||||
distance (i.e., best match first).
|
||||
- A recommendation, one of RECOMMEND_STRONG, RECOMMEND_MEDIUM,
|
||||
or RECOMMEND_NONE; indicating that the first candidate is
|
||||
very likely, it is somewhat likely, or no conclusion could
|
||||
|
|
@ -404,7 +404,7 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
else:
|
||||
id_info = match_by_id(items)
|
||||
if id_info:
|
||||
validate_candidate(items, candidates, id_info)
|
||||
_add_candidate(items, candidates, id_info)
|
||||
rec = recommendation(candidates.values())
|
||||
log.debug('Album ID match recommendation is ' + str(rec))
|
||||
if candidates and not timid:
|
||||
|
|
@ -439,7 +439,7 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
va_likely)
|
||||
log.debug(u'Evaluating %i candidates.' % len(search_cands))
|
||||
for info in search_cands:
|
||||
validate_candidate(items, candidates, info)
|
||||
_add_candidate(items, candidates, info)
|
||||
|
||||
# Sort and get the recommendation.
|
||||
candidates = sorted(candidates.itervalues())
|
||||
|
|
@ -449,10 +449,10 @@ def tag_album(items, timid=False, search_artist=None, search_album=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 title; likewise `search_id`.
|
||||
`(candidates, recommendation)` pair where `candidates` is a list of
|
||||
TrackMatch objects. `search_artist` and `search_title` may be used
|
||||
to override the current metadata for the purposes of the MusicBrainz
|
||||
title; likewise `search_id`.
|
||||
"""
|
||||
# Holds candidates found so far: keys are MBIDs; values are
|
||||
# (distance, TrackInfo) pairs.
|
||||
|
|
@ -465,7 +465,8 @@ def tag_item(item, timid=False, search_artist=None, search_title=None,
|
|||
track_info = hooks._track_for_id(trackid)
|
||||
if track_info:
|
||||
dist = track_distance(item, track_info, incl_artist=True)
|
||||
candidates[track_info.track_id] = (dist, track_info)
|
||||
candidates[track_info.track_id] = \
|
||||
hooks.TrackMatch(dist, track_info)
|
||||
# If this is a good match, then don't keep searching.
|
||||
rec = recommendation(candidates.values())
|
||||
if rec == RECOMMEND_STRONG and not timid:
|
||||
|
|
@ -487,7 +488,7 @@ def tag_item(item, timid=False, search_artist=None, search_title=None,
|
|||
# Get and evaluate candidate metadata.
|
||||
for track_info in hooks._item_candidates(item, search_artist, search_title):
|
||||
dist = track_distance(item, track_info, incl_artist=True)
|
||||
candidates[track_info.track_id] = (dist, track_info)
|
||||
candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info)
|
||||
|
||||
# Sort by distance and return with recommendation.
|
||||
log.debug('Found %i candidates.' % len(candidates))
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ class ImportTask(object):
|
|||
obj.is_album = False
|
||||
return obj
|
||||
|
||||
def set_match(self, cur_artist, cur_album, candidates, rec):
|
||||
def set_candidates(self, cur_artist, cur_album, candidates, rec):
|
||||
"""Sets the candidates for this album matched by the
|
||||
`autotag.tag_album` method.
|
||||
"""
|
||||
|
|
@ -327,45 +327,39 @@ class ImportTask(object):
|
|||
self.candidates = candidates
|
||||
self.rec = rec
|
||||
|
||||
def set_null_match(self):
|
||||
def set_null_candidates(self):
|
||||
"""Set the candidates to indicate no album match was found.
|
||||
"""
|
||||
self.set_match(None, None, None, None)
|
||||
self.cur_artist = None
|
||||
self.cur_album = None
|
||||
self.candidates = None
|
||||
self.rec = None
|
||||
|
||||
def set_item_match(self, candidates, rec):
|
||||
def set_item_candidates(self, candidates, rec):
|
||||
"""Set the match for a single-item task."""
|
||||
assert not self.is_album
|
||||
assert self.item is not None
|
||||
self.item_match = (candidates, rec)
|
||||
|
||||
def set_null_item_match(self):
|
||||
"""For single-item tasks, mark the item as having no matches.
|
||||
"""
|
||||
assert not self.is_album
|
||||
assert self.item is not None
|
||||
self.item_match = None
|
||||
self.candidates = candidates
|
||||
self.rec = rec
|
||||
|
||||
def set_choice(self, choice):
|
||||
"""Given either an (info, items) tuple or an action constant,
|
||||
indicates that an action has been selected by the user (or
|
||||
automatically).
|
||||
"""Given an AlbumMatch or TrackMatch object or an action constant,
|
||||
indicates that an action has been selected for this task.
|
||||
"""
|
||||
assert not self.sentinel
|
||||
# Not part of the task structure:
|
||||
assert choice not in (action.MANUAL, action.MANUAL_ID)
|
||||
assert choice != action.APPLY # Only used internally.
|
||||
assert choice != action.APPLY # Only used internally.
|
||||
if choice in (action.SKIP, action.ASIS, action.TRACKS):
|
||||
self.choice_flag = choice
|
||||
self.info = None
|
||||
self.match = None
|
||||
else:
|
||||
assert not isinstance(choice, action)
|
||||
if self.is_album:
|
||||
info, items = choice
|
||||
self.items = items # Reordered items list.
|
||||
assert isinstance(choice, autotag.AlbumMatch)
|
||||
else:
|
||||
info = choice
|
||||
self.info = info
|
||||
self.choice_flag = action.APPLY # Implicit choice.
|
||||
assert isinstance(choice, autotag.TrackMatch)
|
||||
self.choice_flag = action.APPLY # Implicit choice.
|
||||
self.match = choice
|
||||
|
||||
def save_progress(self):
|
||||
"""Updates the progress state to indicate that this album has
|
||||
|
|
@ -418,20 +412,19 @@ class ImportTask(object):
|
|||
if self.choice_flag is action.ASIS:
|
||||
return (self.cur_artist, self.cur_album)
|
||||
elif self.choice_flag is action.APPLY:
|
||||
return (self.info.artist, self.info.album)
|
||||
return (self.match.info.artist, self.match.info.album)
|
||||
else:
|
||||
if self.choice_flag is action.ASIS:
|
||||
return (self.item.artist, self.item.title)
|
||||
elif self.choice_flag is action.APPLY:
|
||||
return (self.info.artist, self.info.title)
|
||||
return (self.match.info.artist, self.match.info.title)
|
||||
|
||||
def all_items(self):
|
||||
"""If this is an album task, returns the list of non-None
|
||||
(non-gap) items. If this is a singleton task, returns a list
|
||||
containing the item.
|
||||
"""If this is an album task, returns the list of items. If this
|
||||
is a singleton task, returns a list containing the item.
|
||||
"""
|
||||
if self.is_album:
|
||||
return [i for i in self.items if i]
|
||||
return list(self.items)
|
||||
else:
|
||||
return [self.item]
|
||||
|
||||
|
|
@ -559,9 +552,9 @@ def initial_lookup(config):
|
|||
|
||||
log.debug('Looking up: %s' % task.path)
|
||||
try:
|
||||
task.set_match(*autotag.tag_album(task.items, config.timid))
|
||||
task.set_candidates(*autotag.tag_album(task.items, config.timid))
|
||||
except autotag.AutotagError:
|
||||
task.set_null_match()
|
||||
task.set_null_candidates()
|
||||
|
||||
def user_query(config):
|
||||
"""A coroutine for interfacing with the user about the tagging
|
||||
|
|
@ -625,7 +618,7 @@ def show_progress(config):
|
|||
log.info(task.path)
|
||||
|
||||
# Behave as if ASIS were selected.
|
||||
task.set_null_match()
|
||||
task.set_null_candidates()
|
||||
task.set_choice(action.ASIS)
|
||||
|
||||
def apply_choices(config):
|
||||
|
|
@ -648,11 +641,11 @@ def apply_choices(config):
|
|||
if task.should_write_tags():
|
||||
if task.is_album:
|
||||
autotag.apply_metadata(
|
||||
task.items, task.info,
|
||||
task.match.info, task.match.mapping,
|
||||
per_disc_numbering=config.per_disc_numbering
|
||||
)
|
||||
else:
|
||||
autotag.apply_item_metadata(task.item, task.info)
|
||||
autotag.apply_item_metadata(task.item, task.match.info)
|
||||
plugins.send('import_task_apply', config=config, task=task)
|
||||
|
||||
# Infer album-level fields.
|
||||
|
|
@ -833,7 +826,7 @@ def item_lookup(config):
|
|||
|
||||
plugins.send('import_task_start', task=task, config=config)
|
||||
|
||||
task.set_item_match(*autotag.tag_item(task.item, config.timid))
|
||||
task.set_item_candidates(*autotag.tag_item(task.item, config.timid))
|
||||
|
||||
def item_query(config):
|
||||
"""A coroutine that queries the user for input on single-item
|
||||
|
|
@ -871,7 +864,7 @@ def item_progress(config):
|
|||
continue
|
||||
|
||||
log.info(displayable_path(task.item.path))
|
||||
task.set_null_item_match()
|
||||
task.set_null_candidates()
|
||||
task.set_choice(action.ASIS)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ def dist_string(dist, color):
|
|||
out = ui.colorize('red', out)
|
||||
return out
|
||||
|
||||
def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
||||
def show_change(cur_artist, cur_album, match, color=True,
|
||||
per_disc_numbering=False):
|
||||
"""Print out a representation of the changes that will be made if
|
||||
tags are changed from (cur_artist, cur_album, items) to info with
|
||||
distance dist.
|
||||
"""Print out a representation of the changes that will be made if an
|
||||
album's tags are changed according to `match`, which must be an AlbumMatch
|
||||
object.
|
||||
"""
|
||||
def show_album(artist, album, partial=False):
|
||||
if artist:
|
||||
|
|
@ -168,7 +168,7 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
|||
TrackInfo object.
|
||||
"""
|
||||
if per_disc_numbering:
|
||||
if info.mediums > 1:
|
||||
if match.info.mediums > 1:
|
||||
return u'{0}-{1}'.format(track_info.medium,
|
||||
track_info.medium_index)
|
||||
else:
|
||||
|
|
@ -176,14 +176,12 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
|||
else:
|
||||
return unicode(track_info.index)
|
||||
|
||||
# Record if the match is partial or not.
|
||||
partial_match = None in items
|
||||
|
||||
# Identify the album in question.
|
||||
if cur_artist != info.artist or \
|
||||
(cur_album != info.album and info.album != VARIOUS_ARTISTS):
|
||||
artist_l, artist_r = cur_artist or '', info.artist
|
||||
album_l, album_r = cur_album or '', info.album
|
||||
if cur_artist != match.info.artist or \
|
||||
(cur_album != match.info.album and
|
||||
match.info.album != VARIOUS_ARTISTS):
|
||||
artist_l, artist_r = cur_artist or '', match.info.artist
|
||||
album_l, album_r = cur_album or '', match.info.album
|
||||
if artist_r == VARIOUS_ARTISTS:
|
||||
# Hide artists for VA releases.
|
||||
artist_l, artist_r = u'', u''
|
||||
|
|
@ -197,8 +195,8 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
|||
print_("To:")
|
||||
show_album(artist_r, album_r)
|
||||
else:
|
||||
message = u"Tagging: %s - %s" % (info.artist, info.album)
|
||||
if partial_match:
|
||||
message = u"Tagging: %s - %s" % (match.info.artist, match.info.album)
|
||||
if match.extra_items or match.extra_tracks:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE)
|
||||
|
|
@ -206,15 +204,12 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
|||
print_(message)
|
||||
|
||||
# Distance/similarity.
|
||||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
|
||||
# Tracks.
|
||||
missing_tracks = []
|
||||
for item, track_info in zip(items, info.tracks):
|
||||
if not item:
|
||||
missing_tracks.append(track_info)
|
||||
continue
|
||||
|
||||
pairs = match.mapping.items()
|
||||
pairs.sort(key=lambda (_, track_info): track_info.index)
|
||||
for item, track_info in pairs:
|
||||
# Get displayable LHS and RHS values.
|
||||
cur_track = unicode(item.track)
|
||||
new_track = format_index(track_info)
|
||||
|
|
@ -258,20 +253,25 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True,
|
|||
if display:
|
||||
print_(line)
|
||||
|
||||
# Missing tracks.
|
||||
for track_info in missing_tracks:
|
||||
# Missing and unmatched tracks.
|
||||
for track_info in match.extra_tracks:
|
||||
line = u' * Missing track: {0} ({1})'.format(track_info.title,
|
||||
format_index(track_info))
|
||||
if color:
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
for item in match.extra_items:
|
||||
line = u' * Unmatched track: {0} ({1})'.format(item.title, item.track)
|
||||
if color:
|
||||
line = ui.colorize('yellow', line)
|
||||
print_(line)
|
||||
|
||||
def show_item_change(item, info, dist, color):
|
||||
def show_item_change(item, match, color):
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
metadata from `info`.
|
||||
metadata from `match`, a TrackMatch object.
|
||||
"""
|
||||
cur_artist, new_artist = item.artist, info.artist
|
||||
cur_title, new_title = item.title, info.title
|
||||
cur_artist, new_artist = item.artist, match.info.artist
|
||||
cur_title, new_title = item.title, match.info.title
|
||||
|
||||
if cur_artist != new_artist or cur_title != new_title:
|
||||
if color:
|
||||
|
|
@ -286,7 +286,7 @@ def show_item_change(item, info, dist, color):
|
|||
else:
|
||||
print_("Tagging track: %s - %s" % (cur_artist, cur_title))
|
||||
|
||||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
print_('(Similarity: %s)' % dist_string(match.distance, color))
|
||||
|
||||
def should_resume(config, path):
|
||||
return ui.input_yn("Import of the directory:\n%s"
|
||||
|
|
@ -309,14 +309,13 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
itemcount=None, per_disc_numbering=False):
|
||||
"""Given a sorted list of candidates, ask the user for a selection
|
||||
of which candidate to use. Applies to both full albums and
|
||||
singletons (tracks). For albums, the candidates are `(dist, items,
|
||||
info)` triples and `cur_artist`, `cur_album`, and `itemcount` must
|
||||
be provided. For singletons, the candidates are `(dist, info)` pairs
|
||||
and `item` must be provided.
|
||||
singletons (tracks). Candidates are either AlbumMatch or TrackMatch
|
||||
objects depending on `singleton`. for albums, `cur_artist`,
|
||||
`cur_album`, and `itemcount` must be provided. For singletons,
|
||||
`item` must be provided.
|
||||
|
||||
Returns the result of the choice, which may SKIP, ASIS, TRACKS, or
|
||||
MANUAL or a candidate. For albums, a candidate is a `(info, items)`
|
||||
pair; for items, it is just a TrackInfo object.
|
||||
MANUAL or a candidate (an AlbumMatch/TrackMatch object).
|
||||
"""
|
||||
# Sanity check.
|
||||
if singleton:
|
||||
|
|
@ -358,10 +357,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
# Is the change good enough?
|
||||
bypass_candidates = False
|
||||
if rec != autotag.RECOMMEND_NONE:
|
||||
if singleton:
|
||||
dist, info = candidates[0]
|
||||
else:
|
||||
dist, items, info = candidates[0]
|
||||
match = candidates[0]
|
||||
bypass_candidates = True
|
||||
|
||||
while True:
|
||||
|
|
@ -372,22 +368,24 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
print_('Finding tags for track "%s - %s".' %
|
||||
(item.artist, item.title))
|
||||
print_('Candidates:')
|
||||
for i, (dist, info) in enumerate(candidates):
|
||||
print_('%i. %s - %s (%s)' % (i+1, info.artist,
|
||||
info.title, dist_string(dist, color)))
|
||||
for i, match in enumerate(candidates):
|
||||
print_('%i. %s - %s (%s)' %
|
||||
(i + 1, match.info.artist, match.info.title,
|
||||
dist_string(match.distance, color)))
|
||||
else:
|
||||
print_('Finding tags for album "%s - %s".' %
|
||||
(cur_artist, cur_album))
|
||||
print_('Candidates:')
|
||||
for i, (dist, items, info) in enumerate(candidates):
|
||||
line = '%i. %s - %s' % (i+1, info.artist, info.album)
|
||||
for i, match in enumerate(candidates):
|
||||
line = '%i. %s - %s' % (i + 1, match.info.artist,
|
||||
match.info.album)
|
||||
|
||||
# Label and year disambiguation, if available.
|
||||
label, year = None, None
|
||||
if info.label:
|
||||
label = info.label
|
||||
if info.year:
|
||||
year = unicode(info.year)
|
||||
if match.info.label:
|
||||
label = match.info.label
|
||||
if match.info.year:
|
||||
year = unicode(match.info.year)
|
||||
if label and year:
|
||||
line += u' [%s, %s]' % (label, year)
|
||||
elif label:
|
||||
|
|
@ -395,10 +393,10 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
elif year:
|
||||
line += u' [%s]' % year
|
||||
|
||||
line += ' (%s)' % dist_string(dist, color)
|
||||
line += ' (%s)' % dist_string(match.distance, color)
|
||||
|
||||
# Point out the partial matches.
|
||||
if None in items:
|
||||
if match.extra_items or match.extra_tracks:
|
||||
warning = PARTIAL_MATCH_MESSAGE
|
||||
if color:
|
||||
warning = ui.colorize('yellow', warning)
|
||||
|
|
@ -428,26 +426,23 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
else: # Numerical selection.
|
||||
else: # Numerical selection.
|
||||
if singleton:
|
||||
dist, info = candidates[sel-1]
|
||||
match = candidates[sel - 1]
|
||||
else:
|
||||
dist, items, info = candidates[sel-1]
|
||||
match = candidates[sel - 1]
|
||||
bypass_candidates = False
|
||||
|
||||
# Show what we're about to do.
|
||||
if singleton:
|
||||
show_item_change(item, info, dist, color)
|
||||
show_item_change(item, match, color)
|
||||
else:
|
||||
show_change(cur_artist, cur_album, items, info, dist, color,
|
||||
show_change(cur_artist, cur_album, match, color,
|
||||
per_disc_numbering)
|
||||
|
||||
# Exact match => tag automatically if we're not in timid mode.
|
||||
if rec == autotag.RECOMMEND_STRONG and not timid:
|
||||
if singleton:
|
||||
return info
|
||||
else:
|
||||
return info, items
|
||||
return match
|
||||
|
||||
# Ask for confirmation.
|
||||
if singleton:
|
||||
|
|
@ -458,10 +453,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
'as Tracks', 'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, color=color)
|
||||
if sel == 'a':
|
||||
if singleton:
|
||||
return info
|
||||
else:
|
||||
return info, items
|
||||
return match
|
||||
elif sel == 'm':
|
||||
pass
|
||||
elif sel == 's':
|
||||
|
|
@ -505,7 +497,7 @@ def manual_id(singleton):
|
|||
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
|
||||
(info, items) pair, ASIS, or SKIP.
|
||||
AlbumMatch object, ASIS, or SKIP.
|
||||
"""
|
||||
# Show what we're tagging.
|
||||
print_()
|
||||
|
|
@ -514,10 +506,9 @@ def choose_match(task, config):
|
|||
if config.quiet:
|
||||
# No input; just make a decision.
|
||||
if task.rec == autotag.RECOMMEND_STRONG:
|
||||
dist, items, info = task.candidates[0]
|
||||
show_change(task.cur_artist, task.cur_album, items, info, dist,
|
||||
config.color)
|
||||
return info, items
|
||||
match = task.candidates[0]
|
||||
show_change(task.cur_artist, task.cur_album, match, config.color)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
||||
|
|
@ -555,25 +546,25 @@ def choose_match(task, config):
|
|||
except autotag.AutotagError:
|
||||
candidates, rec = None, None
|
||||
else:
|
||||
# We have a candidate! Finish tagging. Here, choice is
|
||||
# an (info, items) pair as desired.
|
||||
assert not isinstance(choice, importer.action)
|
||||
# We have a candidate! Finish tagging. Here, choice is an
|
||||
# AlbumMatch object.
|
||||
assert isinstance(choice, autotag.AlbumMatch)
|
||||
return choice
|
||||
|
||||
def choose_item(task, config):
|
||||
"""Ask the user for a choice about tagging a single item. Returns
|
||||
either an action constant or a TrackInfo object.
|
||||
either an action constant or a TrackMatch object.
|
||||
"""
|
||||
print_()
|
||||
print_(task.item.path)
|
||||
candidates, rec = task.item_match
|
||||
candidates, rec = task.candidates, task.rec
|
||||
|
||||
if config.quiet:
|
||||
# Quiet mode; make a decision.
|
||||
if rec == autotag.RECOMMEND_STRONG:
|
||||
dist, track_info = candidates[0]
|
||||
show_item_change(task.item, track_info, dist, config.color)
|
||||
return track_info
|
||||
match = candidates[0]
|
||||
show_item_change(task.item, match, config.color)
|
||||
return match
|
||||
else:
|
||||
return _quiet_fall_back(config)
|
||||
|
||||
|
|
@ -596,10 +587,10 @@ def choose_item(task, config):
|
|||
search_id = manual_id(True)
|
||||
if search_id:
|
||||
candidates, rec = autotag.tag_item(task.item, config.timid,
|
||||
search_id=search_id)
|
||||
search_id=search_id)
|
||||
else:
|
||||
# Chose a candidate.
|
||||
assert not isinstance(choice, importer.action)
|
||||
assert isinstance(choice, autotag.TrackMatch)
|
||||
return choice
|
||||
|
||||
def resolve_duplicate(task, config):
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import _common
|
||||
from _common import unittest
|
||||
from beetsplug import fetchart
|
||||
from beets.autotag import AlbumInfo
|
||||
from beets.autotag import AlbumInfo, AlbumMatch
|
||||
from beets import library
|
||||
from beets import importer
|
||||
import os
|
||||
|
|
@ -227,7 +227,7 @@ class ArtImporterTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
artist_id = 'artistid',
|
||||
tracks = [],
|
||||
)
|
||||
self.task.set_choice((info, [self.i]))
|
||||
self.task.set_choice(AlbumMatch(0, info, {}, set(), set()))
|
||||
|
||||
def tearDown(self):
|
||||
fetchart.art_for_album = self.old_afa
|
||||
|
|
|
|||
|
|
@ -111,6 +111,15 @@ class TrackDistanceTest(unittest.TestCase):
|
|||
self.assertEqual(dist, 0.0)
|
||||
|
||||
class AlbumDistanceTest(unittest.TestCase):
|
||||
def _mapping(self, items, info):
|
||||
out = {}
|
||||
for i, t in zip(items, info.tracks):
|
||||
out[i] = t
|
||||
return out
|
||||
|
||||
def _dist(self, items, info):
|
||||
return match.distance(items, info, self._mapping(items, info))
|
||||
|
||||
def test_identical_albums(self):
|
||||
items = []
|
||||
items.append(_make_item('one', 1))
|
||||
|
|
@ -123,7 +132,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = False,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertEqual(match.distance(items, info), 0)
|
||||
self.assertEqual(self._dist(items, info), 0)
|
||||
|
||||
def test_incomplete_album(self):
|
||||
items = []
|
||||
|
|
@ -136,9 +145,10 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = False,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertNotEqual(match.distance(items, info), 0)
|
||||
dist = self._dist(items, info)
|
||||
self.assertNotEqual(dist, 0)
|
||||
# Make sure the distance is not too great
|
||||
self.assertTrue(match.distance(items, info) < 0.2)
|
||||
self.assertTrue(dist < 0.2)
|
||||
|
||||
def test_global_artists_differ(self):
|
||||
items = []
|
||||
|
|
@ -152,7 +162,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = False,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertNotEqual(match.distance(items, info), 0)
|
||||
self.assertNotEqual(self._dist(items, info), 0)
|
||||
|
||||
def test_comp_track_artists_match(self):
|
||||
items = []
|
||||
|
|
@ -166,7 +176,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = True,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertEqual(match.distance(items, info), 0)
|
||||
self.assertEqual(self._dist(items, info), 0)
|
||||
|
||||
def test_comp_no_track_artists(self):
|
||||
# Some VA releases don't have track artists (incomplete metadata).
|
||||
|
|
@ -184,7 +194,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
info.tracks[0].artist = None
|
||||
info.tracks[1].artist = None
|
||||
info.tracks[2].artist = None
|
||||
self.assertEqual(match.distance(items, info), 0)
|
||||
self.assertEqual(self._dist(items, info), 0)
|
||||
|
||||
def test_comp_track_artists_do_not_match(self):
|
||||
items = []
|
||||
|
|
@ -198,7 +208,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = True,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertNotEqual(match.distance(items, info), 0)
|
||||
self.assertNotEqual(self._dist(items, info), 0)
|
||||
|
||||
def test_tracks_out_of_order(self):
|
||||
items = []
|
||||
|
|
@ -212,7 +222,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
va = False,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
dist = match.distance(items, info)
|
||||
dist = self._dist(items, info)
|
||||
self.assertTrue(0 < dist < 0.2)
|
||||
|
||||
def test_two_medium_release(self):
|
||||
|
|
@ -230,7 +240,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
info.tracks[0].medium_index = 1
|
||||
info.tracks[1].medium_index = 2
|
||||
info.tracks[2].medium_index = 1
|
||||
dist = match.distance(items, info)
|
||||
dist = self._dist(items, info)
|
||||
self.assertEqual(dist, 0)
|
||||
|
||||
def test_per_medium_track_numbers(self):
|
||||
|
|
@ -248,7 +258,7 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
info.tracks[0].medium_index = 1
|
||||
info.tracks[1].medium_index = 2
|
||||
info.tracks[2].medium_index = 1
|
||||
dist = match.distance(items, info)
|
||||
dist = self._dist(items, info)
|
||||
self.assertEqual(dist, 0)
|
||||
|
||||
def _mkmp3(path):
|
||||
|
|
@ -472,7 +482,15 @@ class AssignmentTest(unittest.TestCase):
|
|||
for item, info in mapping.iteritems():
|
||||
self.assertEqual(items.index(item), trackinfo.index(info))
|
||||
|
||||
class ApplyTest(unittest.TestCase):
|
||||
class ApplyTestUtil(object):
|
||||
def _apply(self, info=None, per_disc_numbering=False):
|
||||
info = info or self.info
|
||||
mapping = {}
|
||||
for i, t in zip(self.items, info.tracks):
|
||||
mapping[i] = t
|
||||
autotag.apply_metadata(info, mapping, per_disc_numbering)
|
||||
|
||||
class ApplyTest(unittest.TestCase, ApplyTestUtil):
|
||||
def setUp(self):
|
||||
self.items = []
|
||||
self.items.append(Item({}))
|
||||
|
|
@ -500,52 +518,51 @@ class ApplyTest(unittest.TestCase):
|
|||
)
|
||||
|
||||
def test_titles_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].title, 'oneNew')
|
||||
self.assertEqual(self.items[1].title, 'twoNew')
|
||||
|
||||
def test_album_and_artist_applied_to_all(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].album, 'albumNew')
|
||||
self.assertEqual(self.items[1].album, 'albumNew')
|
||||
self.assertEqual(self.items[0].artist, 'artistNew')
|
||||
self.assertEqual(self.items[1].artist, 'artistNew')
|
||||
|
||||
def test_track_index_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].track, 1)
|
||||
self.assertEqual(self.items[1].track, 2)
|
||||
|
||||
def test_track_total_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].tracktotal, 2)
|
||||
self.assertEqual(self.items[1].tracktotal, 2)
|
||||
|
||||
def test_disc_index_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].disc, 1)
|
||||
self.assertEqual(self.items[1].disc, 2)
|
||||
|
||||
def test_disc_total_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].disctotal, 2)
|
||||
self.assertEqual(self.items[1].disctotal, 2)
|
||||
|
||||
def test_per_disc_numbering(self):
|
||||
autotag.apply_metadata(self.items, self.info,
|
||||
per_disc_numbering=True)
|
||||
self._apply(per_disc_numbering=True)
|
||||
self.assertEqual(self.items[0].track, 1)
|
||||
self.assertEqual(self.items[1].track, 1)
|
||||
|
||||
def test_mb_trackid_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].mb_trackid,
|
||||
'dfa939ec-118c-4d0f-84a0-60f3d1e6522c')
|
||||
self.assertEqual(self.items[1].mb_trackid,
|
||||
'40130ed1-a27c-42fd-a328-1ebefb6caef4')
|
||||
|
||||
def test_mb_albumid_and_artistid_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
for item in self.items:
|
||||
self.assertEqual(item.mb_albumid,
|
||||
'7edb51cb-77d6-4416-a23c-3a8c2994a2c7')
|
||||
|
|
@ -553,13 +570,13 @@ class ApplyTest(unittest.TestCase):
|
|||
'a6623d39-2d8e-4f70-8242-0a9553b91e50')
|
||||
|
||||
def test_albumtype_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].albumtype, 'album')
|
||||
self.assertEqual(self.items[1].albumtype, 'album')
|
||||
|
||||
def test_album_artist_overrides_empty_track_artist(self):
|
||||
my_info = copy.deepcopy(self.info)
|
||||
autotag.apply_metadata(self.items, my_info)
|
||||
self._apply(info=my_info)
|
||||
self.assertEqual(self.items[0].artist, 'artistNew')
|
||||
self.assertEqual(self.items[0].artist, 'artistNew')
|
||||
|
||||
|
|
@ -567,25 +584,25 @@ class ApplyTest(unittest.TestCase):
|
|||
my_info = copy.deepcopy(self.info)
|
||||
my_info.tracks[0].artist = 'artist1!'
|
||||
my_info.tracks[1].artist = 'artist2!'
|
||||
autotag.apply_metadata(self.items, my_info)
|
||||
self._apply(info=my_info)
|
||||
self.assertEqual(self.items[0].artist, 'artist1!')
|
||||
self.assertEqual(self.items[1].artist, 'artist2!')
|
||||
|
||||
def test_artist_credit_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].albumartist_credit, 'albumArtistCredit')
|
||||
self.assertEqual(self.items[0].artist_credit, 'trackArtistCredit')
|
||||
self.assertEqual(self.items[1].albumartist_credit, 'albumArtistCredit')
|
||||
self.assertEqual(self.items[1].artist_credit, 'albumArtistCredit')
|
||||
|
||||
def test_artist_sort_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].albumartist_sort, 'albumArtistSort')
|
||||
self.assertEqual(self.items[0].artist_sort, 'trackArtistSort')
|
||||
self.assertEqual(self.items[1].albumartist_sort, 'albumArtistSort')
|
||||
self.assertEqual(self.items[1].artist_sort, 'albumArtistSort')
|
||||
|
||||
class ApplyCompilationTest(unittest.TestCase):
|
||||
class ApplyCompilationTest(unittest.TestCase, ApplyTestUtil):
|
||||
def setUp(self):
|
||||
self.items = []
|
||||
self.items.append(Item({}))
|
||||
|
|
@ -616,14 +633,14 @@ class ApplyCompilationTest(unittest.TestCase):
|
|||
)
|
||||
|
||||
def test_album_and_track_artists_separate(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].artist, 'artistOneNew')
|
||||
self.assertEqual(self.items[1].artist, 'artistTwoNew')
|
||||
self.assertEqual(self.items[0].albumartist, 'variousNew')
|
||||
self.assertEqual(self.items[1].albumartist, 'variousNew')
|
||||
|
||||
def test_mb_albumartistid_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertEqual(self.items[0].mb_albumartistid,
|
||||
'89ad4ac3-39f7-470e-963a-56509c546377')
|
||||
self.assertEqual(self.items[1].mb_albumartistid,
|
||||
|
|
@ -634,14 +651,14 @@ class ApplyCompilationTest(unittest.TestCase):
|
|||
'80b3cf5e-18fe-4c59-98c7-e5bb87210710')
|
||||
|
||||
def test_va_flag_cleared_does_not_set_comp(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self._apply()
|
||||
self.assertFalse(self.items[0].comp)
|
||||
self.assertFalse(self.items[1].comp)
|
||||
|
||||
def test_va_flag_sets_comp(self):
|
||||
va_info = copy.deepcopy(self.info)
|
||||
va_info.va = True
|
||||
autotag.apply_metadata(self.items, va_info)
|
||||
self._apply(info=va_info)
|
||||
self.assertTrue(self.items[0].comp)
|
||||
self.assertTrue(self.items[1].comp)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from _common import unittest
|
|||
from beets import library
|
||||
from beets import importer
|
||||
from beets import mediafile
|
||||
from beets.autotag import AlbumInfo, TrackInfo
|
||||
from beets.autotag import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch
|
||||
|
||||
TEST_TITLES = ('The Opener', 'The Second Track', 'The Last Track')
|
||||
class NonAutotaggedImportTest(unittest.TestCase):
|
||||
|
|
@ -182,7 +182,8 @@ def _call_stages(config, items, choice_or_info,
|
|||
if isinstance(choice_or_info, importer.action):
|
||||
task.set_choice(choice_or_info)
|
||||
else:
|
||||
task.set_choice((choice_or_info, items))
|
||||
mapping = dict(zip(items, choice_or_info.tracks))
|
||||
task.set_choice(AlbumMatch(0, choice_or_info, mapping, set(), set()))
|
||||
|
||||
# Call the coroutines.
|
||||
for stage in stages:
|
||||
|
|
@ -265,7 +266,7 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
manip_coro.next()
|
||||
|
||||
task = importer.ImportTask.item_task(self.i)
|
||||
task.set_choice(self.info.tracks[0])
|
||||
task.set_choice(TrackMatch(0, self.info.tracks[0]))
|
||||
apply_coro.send(task)
|
||||
manip_coro.send(task)
|
||||
|
||||
|
|
@ -579,7 +580,7 @@ class InferAlbumDataTest(unittest.TestCase):
|
|||
|
||||
self.task = importer.ImportTask(path='a path', toppath='top path',
|
||||
items=self.items)
|
||||
self.task.set_null_match()
|
||||
self.task.set_null_candidates()
|
||||
|
||||
def _infer(self):
|
||||
importer._infer_album_fields(self.task)
|
||||
|
|
@ -621,7 +622,7 @@ class InferAlbumDataTest(unittest.TestCase):
|
|||
self.assertEqual(self.items[0].albumartist, self.items[2].artist)
|
||||
|
||||
def test_apply_gets_artist_and_id(self):
|
||||
self.task.set_choice(({}, self.items)) # APPLY
|
||||
self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY
|
||||
|
||||
self._infer()
|
||||
|
||||
|
|
@ -633,7 +634,7 @@ class InferAlbumDataTest(unittest.TestCase):
|
|||
for item in self.items:
|
||||
item.albumartist = 'some album artist'
|
||||
item.mb_albumartistid = 'some album artist id'
|
||||
self.task.set_choice(({}, self.items)) # APPLY
|
||||
self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY
|
||||
|
||||
self._infer()
|
||||
|
||||
|
|
@ -651,7 +652,7 @@ class InferAlbumDataTest(unittest.TestCase):
|
|||
|
||||
def test_first_item_null_apply(self):
|
||||
self.items[0] = None
|
||||
self.task.set_choice(({}, self.items)) # APPLY
|
||||
self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY
|
||||
self._infer()
|
||||
self.assertFalse(self.items[1].comp)
|
||||
self.assertEqual(self.items[1].albumartist, self.items[2].artist)
|
||||
|
|
@ -672,14 +673,12 @@ class DuplicateCheckTest(unittest.TestCase):
|
|||
|
||||
task = importer.ImportTask(path='a path', toppath='top path',
|
||||
items=[item])
|
||||
task.set_match(artist, album, None, None)
|
||||
task.set_candidates(artist, album, None, None)
|
||||
if asis:
|
||||
task.set_choice(importer.action.ASIS)
|
||||
else:
|
||||
task.set_choice((
|
||||
AlbumInfo(album, None, artist, None, None),
|
||||
[item]
|
||||
))
|
||||
info = AlbumInfo(album, None, artist, None, None)
|
||||
task.set_choice(AlbumMatch(0, info, {}, set(), set()))
|
||||
return task
|
||||
|
||||
def _item_task(self, asis, artist=None, title=None, existing=False):
|
||||
|
|
@ -696,7 +695,7 @@ class DuplicateCheckTest(unittest.TestCase):
|
|||
item.title = title
|
||||
task.set_choice(importer.action.ASIS)
|
||||
else:
|
||||
task.set_choice(TrackInfo(title, None, artist))
|
||||
task.set_choice(TrackMatch(0, TrackInfo(title, None, artist)))
|
||||
return task
|
||||
|
||||
def test_duplicate_album_apply(self):
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ class AutotagTest(unittest.TestCase):
|
|||
'path',
|
||||
[_common.item()],
|
||||
)
|
||||
task.set_match('artist', 'album', [], autotag.RECOMMEND_NONE)
|
||||
task.set_candidates('artist', 'album', [], autotag.RECOMMEND_NONE)
|
||||
res = commands.choose_match(task, _common.iconfig(None, quiet=False))
|
||||
self.assertEqual(res, result)
|
||||
self.assertTrue('No match' in self.io.getoutput())
|
||||
|
|
@ -647,72 +647,66 @@ class ShowChangeTest(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.io = _common.DummyIO()
|
||||
self.io.install()
|
||||
def tearDown(self):
|
||||
self.io.restore()
|
||||
|
||||
def _items_and_info(self):
|
||||
items = [_common.item()]
|
||||
items[0].track = 1
|
||||
items[0].path = '/path/to/file.mp3'
|
||||
info = autotag.AlbumInfo(
|
||||
self.items = [_common.item()]
|
||||
self.items[0].track = 1
|
||||
self.items[0].path = '/path/to/file.mp3'
|
||||
self.info = autotag.AlbumInfo(
|
||||
'the album', 'album id', 'the artist', 'artist id', [
|
||||
autotag.TrackInfo('the title', 'track id', index=1)
|
||||
])
|
||||
return items, info
|
||||
|
||||
def tearDown(self):
|
||||
self.io.restore()
|
||||
|
||||
def _show_change(self, items=None, info=None,
|
||||
cur_artist='the artist', cur_album='the album',
|
||||
dist=0.1):
|
||||
items = items or self.items
|
||||
info = info or self.info
|
||||
mapping = dict(zip(items, info.tracks))
|
||||
commands.show_change(
|
||||
cur_artist,
|
||||
cur_album,
|
||||
autotag.AlbumMatch(0.1, info, mapping, set(), set()),
|
||||
color=False,
|
||||
)
|
||||
return self.io.getoutput().lower()
|
||||
|
||||
def test_null_change(self):
|
||||
items, info = self._items_and_info()
|
||||
commands.show_change('the artist', 'the album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
msg = self._show_change()
|
||||
self.assertTrue('similarity: 90' in msg)
|
||||
self.assertTrue('tagging:' in msg)
|
||||
|
||||
def test_album_data_change(self):
|
||||
items, info = self._items_and_info()
|
||||
commands.show_change('another artist', 'another album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
msg = self._show_change(cur_artist='another artist',
|
||||
cur_album='another album')
|
||||
self.assertTrue('correcting tags from:' in msg)
|
||||
|
||||
def test_item_data_change(self):
|
||||
items, info = self._items_and_info()
|
||||
items[0].title = 'different'
|
||||
commands.show_change('the artist', 'the album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
self.items[0].title = 'different'
|
||||
msg = self._show_change()
|
||||
self.assertTrue('different -> the title' in msg)
|
||||
|
||||
def test_item_data_change_with_unicode(self):
|
||||
items, info = self._items_and_info()
|
||||
items[0].title = u'caf\xe9'
|
||||
commands.show_change('the artist', 'the album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
self.items[0].title = u'caf\xe9'
|
||||
msg = self._show_change()
|
||||
self.assertTrue(u'caf\xe9 -> the title' in msg.decode('utf8'))
|
||||
|
||||
def test_album_data_change_with_unicode(self):
|
||||
items, info = self._items_and_info()
|
||||
commands.show_change(u'caf\xe9', u'another album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
msg = self._show_change(cur_artist=u'caf\xe9',
|
||||
cur_album=u'another album')
|
||||
self.assertTrue('correcting tags from:' in msg)
|
||||
|
||||
def test_item_data_change_title_missing(self):
|
||||
items, info = self._items_and_info()
|
||||
items[0].title = ''
|
||||
commands.show_change('the artist', 'the album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
self.items[0].title = ''
|
||||
msg = self._show_change()
|
||||
self.assertTrue('file.mp3 -> the title' in msg)
|
||||
|
||||
def test_item_data_change_title_missing_with_unicode_filename(self):
|
||||
items, info = self._items_and_info()
|
||||
items[0].title = ''
|
||||
items[0].path = u'/path/to/caf\xe9.mp3'.encode('utf8')
|
||||
commands.show_change('the artist', 'the album',
|
||||
items, info, 0.1, color=False)
|
||||
msg = self.io.getoutput().lower()
|
||||
self.items[0].title = ''
|
||||
self.items[0].path = u'/path/to/caf\xe9.mp3'.encode('utf8')
|
||||
msg = self._show_change()
|
||||
self.assertTrue(u'caf\xe9.mp3 -> the title' in msg.decode('utf8'))
|
||||
|
||||
class DefaultPathTest(unittest.TestCase):
|
||||
|
|
|
|||
Loading…
Reference in a new issue