diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 9306ee237..e636e4b5b 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -17,14 +17,20 @@ """Facilities for automatically determining files' correct metadata. """ +import os from collections import defaultdict from beets.autotag.mb import match_album +from beets import library +from beets.mediafile import FileTypeError + +# If the MusicBrainz length is more than this many seconds away from the +# track length, an error is reported. +LENGTH_TOLERANCE = 2 def likely_metadata(items): """Returns the most likely artist and album for a set of Items. Each is determined by tag reflected by the plurality of the Items. """ - # The tags we'll try to determine. keys = 'artist', 'album' @@ -54,11 +60,121 @@ def likely_metadata(items): return (likelies['artist'], likelies['album']) -if __name__ == '__main__': # Smoke test. - from beets.library import Item - items = [Item({'artist': 'The Beatles', 'album': 'The White Album'}), - Item({'artist': 'The Beetles', 'album': 'The White Album'}), - Item({'artist': 'The Beatles', 'album': 'Teh White Album'})] - print likely_metadata(items) +def _input_yn(prompt): + """Prompts user for a "yes" or "no" response where an empty response + is treated as "yes". Keeps prompting until acceptable input is + given; returns a boolean. + """ + resp = raw_input(prompt) + while True: + if len(resp) == 0 or resp[0].tolower() == 'y': + return True + elif len(resp) > 0 and resp[0].tolower() == 'n': + return False + resp = raw_input("Type 'y' or 'n': ") + +def tag_album_dir(path, lib): + # Read items from directory. + items = [] + for filename in os.listdir(path): + filepath = library._normpath(os.path.join(path, filename)) + try: + i = library.Item.from_path(filepath, lib) + except FileTypeError: + continue + items.append(i) + + #fixme Check if MB tags are already present. + + # Find existing metadata. + cur_artist, cur_album = likely_metadata(items) + + # Find "correct" metadata. + info = match_album(cur_artist, cur_album, len(items)) + if len(cur_artist) == 0 or len(cur_album) == 0 or \ + cur_artist.lower() != info['artist'].lower() or \ + cur_album.lower() != info['album'].lower(): + # If we're making a "significant" change (changing the artist or + # album), confirm with the user to avoid mistakes. + print "Correcting tags from:" + print '%s - %s' % (cur_artist, cur_album) + print "To:" + print '%s - %s' % (info['artist'], info['album']) + if not _input_yn("Apply change ([y]/n)? "): + return + + else: + print 'Tagging album: %s - %s' % (info['artist'], info['album']) + + + # Ensure that we don't have the album already. + q = library.AndQuery((library.MatchQuery('artist', info['artist']), + library.MatchQuery('album', info['album']))) + count, _ = q.count(lib) + if count >= 1: + print "This album (%s - %s) is already in the library!" % \ + (info['artist'], info['album']) + return + + # Determine order of existing tracks. + # First, see if the current tags indicate an ordering. + ordered_items = [None]*len(items) + available_indices = set(range(len(items))) + for item in items: + if item.track: + index = item.track - 1 + ordered_items[index] = item + available_indices.remove(index) + else: + # If we have any item without an index, give up. + break + if available_indices: + print "Tracks are not correctly ordered." + return + #fixme: + # Otherwise, match based on names and lengths of tracks (confirm). + + # Apply new metadata. + for index, (item, track_data) in enumerate(zip(ordered_items, + info['tracks'] + )): + + # For safety, ensure track lengths match. + if not (item.length - LENGTH_TOLERANCE < + track_data['length'] < + item.length + LENGTH_TOLERANCE): + print "Length mismatch on track %i: actual length is %f and MB " \ + "length is %f." % (index, item.length, track_data['length']) + return + + if item.title != track_data['title']: + print "%s -> %s" % (item.title, track_data['title']) + + item.artist = info['artist'] + item.album = info['album'] + item.track_total = len(items) + item.year = info['year'] + if 'month' in info: + item.month = info['month'] + if 'day' in info: + item.day = info['day'] + + item.title = track_data['title'] + item.track = index + 1 + + #fixme Set MusicBrainz IDs! + + # Add items to library and write their tags. + for item in ordered_items: + item.move(True) + item.add() + item.write() +if __name__ == '__main__': + import sys + lib = library.Library() + path = os.path.expanduser(sys.argv[1]) + tag_album_dir(path, lib) + lib.save() + diff --git a/bts b/bts index e5bd982d1..63a7b67f3 100755 --- a/bts +++ b/bts @@ -18,6 +18,7 @@ from optparse import OptionParser from beets import Library +from beets import autotag from ConfigParser import SafeConfigParser import os @@ -86,6 +87,11 @@ def read(lib, config, criteria): item.store() lib.save() +def tagalbum(lib, config, paths): + for path in paths: + autotag.tag_album_dir(os.path.expanduser(path), lib) + lib.save() + def bpd(lib, config, opts): host = opts.pop(0) if opts else None host = host if host else config.get('bpd', 'host') @@ -99,7 +105,7 @@ def bpd(lib, config, opts): if __name__ == "__main__": # parse options usage = """usage: %prog [options] command -command is one of: add, remove, update, write, list, bpd, help""" +command is one of: add, remove, update, write, list, bpd, tagalbum, help""" op = OptionParser(usage=usage) op.add_option('-l', '--library', dest='libpath', metavar='PATH', default=None, @@ -132,6 +138,7 @@ command is one of: add, remove, update, write, list, bpd, help""" (imp, ['import', 'im', 'imp']), (remove, ['remove', 'rm']), (delete, ['delete', 'del']), + (tagalbum, ['tagalbum', 'tag']), (read, ['read', 'r']), #(write, ['write', 'wr', 'w']),