mirror of
https://github.com/beetbox/beets.git
synced 2026-01-02 22:12:53 +01:00
Merge pull request #13 from laarmen/feature/incomplete_albums
Tag incomplete albums (#260 on Google Code)
This commit is contained in:
commit
0b5a47a745
5 changed files with 89 additions and 16 deletions
|
|
@ -119,6 +119,8 @@ def apply_metadata(items, album_info):
|
|||
"""
|
||||
for index, (item, track_info) in enumerate(zip(items, album_info.tracks)):
|
||||
# Album, artist, track count.
|
||||
if not item:
|
||||
continue
|
||||
if track_info.artist:
|
||||
item.artist = track_info.artist
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ releases and tracks.
|
|||
"""
|
||||
import logging
|
||||
import re
|
||||
import copy
|
||||
from munkres import Munkres
|
||||
from unidecode import unidecode
|
||||
|
||||
|
|
@ -31,6 +32,8 @@ ARTIST_WEIGHT = 3.0
|
|||
ALBUM_WEIGHT = 3.0
|
||||
# The weight of the entire distance calculated for a given track.
|
||||
TRACK_WEIGHT = 1.0
|
||||
# The weight of a missing track.
|
||||
MISSING_WEIGHT = 0.3
|
||||
# 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).
|
||||
|
|
@ -161,7 +164,7 @@ def current_metadata(items):
|
|||
likelies = {}
|
||||
consensus = {}
|
||||
for key in keys:
|
||||
values = [getattr(item, key) for item in items]
|
||||
values = [getattr(item, key) for item in items if item]
|
||||
likelies[key], freq = plurality(values)
|
||||
consensus[key] = (freq == len(values))
|
||||
return likelies['artist'], likelies['album'], consensus['artist']
|
||||
|
|
@ -171,8 +174,9 @@ def order_items(items, trackinfo):
|
|||
information. This always produces a result if the numbers of tracks
|
||||
match.
|
||||
"""
|
||||
# Make sure lengths match.
|
||||
if len(items) != len(trackinfo):
|
||||
# Make sure lengths match: If there is less items, it might just be that
|
||||
# there is some tracks missing.
|
||||
if len(items) > len(trackinfo):
|
||||
return None
|
||||
|
||||
# Construct the cost matrix.
|
||||
|
|
@ -187,7 +191,7 @@ def order_items(items, trackinfo):
|
|||
matching = Munkres().compute(costs)
|
||||
|
||||
# Order items based on the matching.
|
||||
ordered_items = [None]*len(items)
|
||||
ordered_items = [None]*len(trackinfo)
|
||||
for cur_idx, canon_idx in matching:
|
||||
ordered_items[canon_idx] = items[cur_idx]
|
||||
return ordered_items
|
||||
|
|
@ -269,12 +273,25 @@ def distance(items, album_info):
|
|||
|
||||
# Track distances.
|
||||
for i, (item, track_info) in enumerate(zip(items, album_info.tracks)):
|
||||
dist += track_distance(item, track_info, i+1, album_info.va) * \
|
||||
TRACK_WEIGHT
|
||||
dist_max += TRACK_WEIGHT
|
||||
if item:
|
||||
dist += track_distance(item, track_info, i+1, album_info.va) * \
|
||||
TRACK_WEIGHT
|
||||
dist_max += TRACK_WEIGHT
|
||||
else:
|
||||
dist += MISSING_WEIGHT
|
||||
dist_max += MISSING_WEIGHT
|
||||
|
||||
# Plugin distances.
|
||||
plugin_d, plugin_dm = plugins.album_distance(items, album_info)
|
||||
# In order not to break compatibility, send purged lists
|
||||
purged_items, purged_tracks = [], []
|
||||
for i, t in zip(items, album_info.tracks):
|
||||
if i:
|
||||
purged_items.append(i)
|
||||
purged_tracks.append(t)
|
||||
purged_album_info = copy.copy(album_info)
|
||||
purged_album_info.tracks = purged_tracks
|
||||
|
||||
plugin_d, plugin_dm = plugins.album_distance(purged_items, purged_album_info)
|
||||
dist += plugin_d
|
||||
dist_max += plugin_dm
|
||||
|
||||
|
|
@ -348,7 +365,7 @@ def validate_candidate(items, tuple_dict, info):
|
|||
return
|
||||
|
||||
# Make sure the album has the correct number of tracks.
|
||||
if len(items) != len(info.tracks):
|
||||
if len(items) > len(info.tracks):
|
||||
log.debug('Track count mismatch.')
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ def _duplicate_check(lib, task, recent=None):
|
|||
recent.add((artist, album))
|
||||
|
||||
# Look in the library.
|
||||
cur_paths = set(i.path for i in task.items)
|
||||
cur_paths = set(i.path for i in task.items if i)
|
||||
for album_cand in lib.albums(artist=artist):
|
||||
if album_cand.album == album:
|
||||
# Check whether the album is identical in contents, in which
|
||||
|
|
@ -585,7 +585,7 @@ def apply_choices(config):
|
|||
if task.should_skip():
|
||||
continue
|
||||
|
||||
items = task.items if task.is_album else [task.item]
|
||||
items = [i for i in task.items if i] if task.is_album else [task.item]
|
||||
# Clear IDs in case the items are being re-tagged.
|
||||
for item in items:
|
||||
item.id = None
|
||||
|
|
@ -637,7 +637,7 @@ def apply_choices(config):
|
|||
# Add new ones.
|
||||
if task.is_album:
|
||||
# Add an album.
|
||||
album = lib.add_album(task.items)
|
||||
album = lib.add_album([i for i in task.items if i])
|
||||
task.album_id = album.id
|
||||
else:
|
||||
# Add tracks.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ DEFAULT_IGNORE = ['.AppleDouble', '._*', '*~', '.DS_Store']
|
|||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
PARTIAL_MATCH_STRING = ui.colorize('green', u'(Partial match !)')
|
||||
|
||||
# Importer utilities and support.
|
||||
|
||||
def dist_string(dist, color):
|
||||
|
|
@ -122,13 +124,22 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
tags are changed from (cur_artist, cur_album, items) to info with
|
||||
distance dist.
|
||||
"""
|
||||
def show_album(artist, album):
|
||||
def show_album(artist, album, partial=False):
|
||||
if artist:
|
||||
print_(' %s - %s' % (artist, album))
|
||||
album_description = ' %s - %s' % (artist, album)
|
||||
elif album:
|
||||
print_(' %s' % album)
|
||||
album_description = ' %s' % album
|
||||
else:
|
||||
print_(' (unknown album)')
|
||||
album_description = ' (unknown album)'
|
||||
|
||||
# Add a suffix indicating a partial match
|
||||
if partial:
|
||||
print_('%s %s' % (album_description, PARTIAL_MATCH_STRING))
|
||||
else:
|
||||
print_(album_description)
|
||||
|
||||
# Record if the match is partial or not.
|
||||
partial_match = None in items
|
||||
|
||||
# Identify the album in question.
|
||||
if cur_artist != info.artist or \
|
||||
|
|
@ -147,6 +158,9 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
show_album(artist_l, album_l)
|
||||
print_("To:")
|
||||
show_album(artist_r, album_r)
|
||||
elif partial_match:
|
||||
print_("Tagging: %s - %s %s" % (info.artist, info.album,
|
||||
PARTIAL_MATCH_STRING))
|
||||
else:
|
||||
print_("Tagging: %s - %s" % (info.artist, info.album))
|
||||
|
||||
|
|
@ -154,7 +168,11 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
print_('(Similarity: %s)' % dist_string(dist, color))
|
||||
|
||||
# Tracks.
|
||||
missing_tracks = []
|
||||
for i, (item, track_info) in enumerate(zip(items, info.tracks)):
|
||||
if not item:
|
||||
missing_tracks.append((i, track_info))
|
||||
continue
|
||||
cur_track = unicode(item.track)
|
||||
new_track = unicode(i+1)
|
||||
cur_title = item.title
|
||||
|
|
@ -179,6 +197,9 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
print_(u" * %s -> %s" % (cur_title, new_title))
|
||||
elif cur_track != new_track:
|
||||
print_(u" * %s (%s -> %s)" % (item.title, cur_track, new_track))
|
||||
for i, track_info in missing_tracks:
|
||||
print_(ui.colorize('red', u' * Missing track: %s (%d)' % \
|
||||
(track_info.title, i+1)))
|
||||
|
||||
def show_item_change(item, info, dist, color):
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
|
|
@ -305,6 +326,11 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
line += u' [%s]' % year
|
||||
|
||||
line += ' (%s)' % dist_string(dist, color)
|
||||
|
||||
# Pointing out the partial matches.
|
||||
if None in items:
|
||||
line += ' %s' % PARTIAL_MATCH_STRING
|
||||
|
||||
print_(line)
|
||||
|
||||
# Ask the user for a choice.
|
||||
|
|
|
|||
|
|
@ -98,6 +98,21 @@ class AlbumDistanceTest(unittest.TestCase):
|
|||
)
|
||||
self.assertEqual(match.distance(items, info), 0)
|
||||
|
||||
def test_incomplete_album(self):
|
||||
items = []
|
||||
items.append(self.item('one', 1))
|
||||
items.append(self.item('three', 3))
|
||||
info = AlbumInfo(
|
||||
artist = 'some artist',
|
||||
album = 'some album',
|
||||
tracks = self.trackinfo(),
|
||||
va = False,
|
||||
album_id = None, artist_id = None,
|
||||
)
|
||||
self.assertNotEqual(match.distance(items, info), 0)
|
||||
# Make sure the distance is not too great
|
||||
self.assertTrue(match.distance(items, info) < 0.2)
|
||||
|
||||
def test_global_artists_differ(self):
|
||||
items = []
|
||||
items.append(self.item('one', 1))
|
||||
|
|
@ -291,11 +306,24 @@ class OrderingTest(unittest.TestCase):
|
|||
items = []
|
||||
items.append(self.item('one', 1))
|
||||
items.append(self.item('two', 2))
|
||||
items.append(self.item('three', 3))
|
||||
items.append(self.item('four',4))
|
||||
trackinfo = []
|
||||
trackinfo.append(TrackInfo('one', None))
|
||||
ordered = match.order_items(items, trackinfo)
|
||||
self.assertEqual(ordered, None)
|
||||
|
||||
def test_order_works_with_missing_tracks(self)
|
||||
items = []
|
||||
items.append(self.item('one', 1))
|
||||
items.append(self.item('two', 2))
|
||||
trackinfo = []
|
||||
trackinfo.append(TrackInfo('one', None))
|
||||
ordered = match.order_items(items, trackinfo)
|
||||
self.assertEqual(ordered[0].title, 'one')
|
||||
self.assertEqual(ordered[1].title, 'two')
|
||||
self.assertEqual(ordered[2], None)
|
||||
|
||||
def test_order_corrects_when_track_names_are_entirely_wrong(self):
|
||||
# A real-world test case contributed by a user.
|
||||
def item(i, length):
|
||||
|
|
|
|||
Loading…
Reference in a new issue