From effc338957cd58da917533df8f2eea6d08fccf7e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 2 Apr 2011 19:59:22 -0700 Subject: [PATCH 1/4] "info" command for dumping file metadata --- NEWS | 1 + beets/ui/commands.py | 1 - beetsplug/info.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 beetsplug/info.py diff --git a/NEWS b/NEWS index 45a50cf4d..78d3b74bd 100644 --- a/NEWS +++ b/NEWS @@ -38,6 +38,7 @@ quiet mode when there is no strong recommendation. The options are "skip" (the default) and "asis". * The "version" command now lists all the loaded plugins. +* A new plugin, called "info", just prints out audio file metadata. * Fix a bug where some files would be erroneously interpreted as MP4. * Fix permission bits applied to album art files. * Fix malformed MusicBrainz queries caused by null characters. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 57abf297b..ae71e60f3 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -25,7 +25,6 @@ from beets import ui from beets.ui import print_ from beets import autotag from beets import library -from beets.mediafile import UnreadableFileError, FileTypeError import beets.autotag.art from beets.ui import pipeline from beets import plugins diff --git a/beetsplug/info.py b/beetsplug/info.py new file mode 100644 index 000000000..5e5fde518 --- /dev/null +++ b/beetsplug/info.py @@ -0,0 +1,57 @@ +# This file is part of beets. +# Copyright 2011, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Shows file metadata. +""" + +from beets.plugins import BeetsPlugin +from beets import library +from beets import ui +from beets import mediafile + +def info(paths): + # Set up fields to output. + fields = [] + for name, _, _, mffield in library.ITEM_FIELDS: + if mffield: + fields.append(name) + maxwidth = max(len(name) for name in fields) + lineformat = u'{{:>{0}}}: {{}}'.format(maxwidth) + + first = True + for path in paths: + if not first: + ui.print_() + + path = library._normpath(path) + ui.print_(path) + try: + mf = mediafile.MediaFile(path) + except mediafile.UnreadableFileError: + ui.print_('cannot read file') + continue + for name in fields: + ui.print_(lineformat.format(name, getattr(mf, name))) + + first = False + +class InfoPlugin(BeetsPlugin): + def commands(self): + cmd = ui.Subcommand('info', help='show file metadata') + def func(lib, config, opts, args): + if not args: + raise ui.UserError('no file specified') + info(args) + cmd.func = func + return [cmd] From f2567269173aee13ad7a9a6e8a0abbc56fb84078 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 2 Apr 2011 20:29:01 -0700 Subject: [PATCH 2/4] explicit searching for VA releases when it seems likely --- beets/autotag/__init__.py | 22 +++++++++++++++++----- beets/autotag/mb.py | 7 ++++++- test/test_autotag.py | 21 +++++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index d60f968df..7b20da3b4 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -74,6 +74,9 @@ SD_PATTERNS = [ (r'(, )?(pt\.|part) .+', 0.2), ] +# Artist signals that indicate "various artists". +VA_ARTISTS = (u'', u'various artists', u'va', u'unknown') + # Autotagging exceptions. class AutotagError(Exception): pass @@ -230,7 +233,7 @@ def _plurality(objs): max_freq = freq res = obj - return res + return res, len(freqs) <= 1 def current_metadata(items): """Returns the most likely artist and album for a set of Items. @@ -238,10 +241,11 @@ def current_metadata(items): """ keys = 'artist', 'album' likelies = {} + consensus = {} for key in keys: values = [getattr(item, key) for item in items] - likelies[key] = _plurality(values) - return likelies['artist'], likelies['album'] + likelies[key], consensus[key] = _plurality(values) + return likelies['artist'], likelies['album'], consensus['artist'] def order_items(items, trackinfo): """Orders the items based on how they match some canonical track @@ -325,7 +329,7 @@ def distance(items, info): """Determines how "significant" an album metadata change would be. Returns a float in [0.0,1.0]. The list of items must be ordered. """ - cur_artist, cur_album = current_metadata(items) + cur_artist, cur_album, _ = current_metadata(items) cur_artist = cur_artist or '' cur_album = cur_album or '' @@ -492,7 +496,7 @@ def tag_album(items, search_artist=None, search_album=None): May raise an AutotagError if existing metadata is insufficient. """ # Get current metadata. - cur_artist, cur_album = current_metadata(items) + cur_artist, cur_album, artist_consensus = current_metadata(items) log.debug('Tagging %s - %s' % (cur_artist, cur_album)) # The output result tuples (keyed by MB album ID). @@ -525,6 +529,14 @@ def tag_album(items, search_artist=None, search_album=None): else: candidates = [] + # Possibly add "various artists" search. + if search_album and ((not artist_consensus) or \ + (search_artist.lower() in VA_ARTISTS) or \ + any(item.comp for item in items)): + log.debug('Possibly Various Artists; adding matches.') + candidates.extend(mb.match_album(None, search_album, len(items), + MAX_CANDIDATES)) + # Get candidates from plugins. candidates.extend(plugins.candidates(items)) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 2894aea18..35fa62131 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -226,7 +226,12 @@ def match_album(artist, album, tracks=None, limit=SEARCH_LIMIT): optionally, a number of tracks on the album. """ # Build search criteria. - criteria = {'artist': artist, 'release': album} + criteria = {'release': album} + if artist is not None: + criteria['artist'] = artist + else: + # Various Artists search. + criteria['arid'] = VARIOUS_ARTISTS_ID if tracks is not None: criteria['tracks'] = str(tracks) diff --git a/test/test_autotag.py b/test/test_autotag.py index ac17abfd8..484cd5ad5 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -28,26 +28,39 @@ from beets.library import Item class PluralityTest(unittest.TestCase): def test_plurality_consensus(self): objs = [1, 1, 1, 1] - obj = autotag._plurality(objs) + obj, consensus = autotag._plurality(objs) self.assertEqual(obj, 1) + self.assertTrue(consensus) def test_plurality_near_consensus(self): objs = [1, 1, 2, 1] - obj = autotag._plurality(objs) + obj, consensus = autotag._plurality(objs) self.assertEqual(obj, 1) + self.assertFalse(consensus) def test_plurality_conflict(self): objs = [1, 1, 2, 2, 3] - obj = autotag._plurality(objs) + obj, consensus = autotag._plurality(objs) self.assert_(obj in (1, 2)) + self.assertFalse(consensus) def test_current_metadata_finds_pluralities(self): items = [Item({'artist': 'The Beetles', 'album': 'The White Album'}), Item({'artist': 'The Beatles', 'album': 'The White Album'}), Item({'artist': 'The Beatles', 'album': 'Teh White Album'})] - l_artist, l_album = autotag.current_metadata(items) + l_artist, l_album, artist_consensus = autotag.current_metadata(items) self.assertEqual(l_artist, 'The Beatles') self.assertEqual(l_album, 'The White Album') + self.assertFalse(artist_consensus) + + def test_current_metadata_artist_consensus(self): + items = [Item({'artist': 'The Beatles', 'album': 'The White Album'}), + Item({'artist': 'The Beatles', 'album': 'The White Album'}), + Item({'artist': 'The Beatles', 'album': 'Teh White Album'})] + l_artist, l_album, artist_consensus = autotag.current_metadata(items) + self.assertEqual(l_artist, 'The Beatles') + self.assertEqual(l_album, 'The White Album') + self.assertTrue(artist_consensus) class AlbumDistanceTest(unittest.TestCase): def item(self, title, track, artist='some artist'): From 1a87483877bd56070e75562e18f55e6adc006ed0 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 2 Apr 2011 20:42:07 -0700 Subject: [PATCH 3/4] hide artist in difference display for VA matches --- beets/ui/commands.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index ae71e60f3..17aea209f 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -50,6 +50,8 @@ DEFAULT_IMPORT_RESUME = None # "ask" DEFAULT_THREADED = True DEFAULT_COLOR = True +VARIOUS_ARTISTS = u'Various Artists' + class ImportAbort(Exception): """Raised when the user aborts the tagging operation. """ @@ -76,16 +78,28 @@ 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. """ - if cur_artist != info['artist'] or cur_album != info['album']: + def show_album(artist, album): + if artist: + print_(' %s - %s' % (artist, album)) + else: + print_(' %s' % album) + + 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 artist_r == VARIOUS_ARTISTS: + # Hide artists for VA releases. + artist_l, artist_r = u'', u'' + if color: artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) + print_("Correcting tags from:") - print_(' %s - %s' % (artist_l, album_l)) + show_album(artist_l, album_l) print_("To:") - print_(' %s - %s' % (artist_r, album_r)) + show_album(artist_r, album_r) else: print_("Tagging: %s - %s" % (info['artist'], info['album'])) print_('(Distance: %s)' % dist_string(dist, color)) From 4193d5411c93821020e332b432336bb5c50245bd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 2 Apr 2011 20:50:27 -0700 Subject: [PATCH 4/4] bump up the pipeline queue size --- beets/ui/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 17aea209f..062a204c1 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2010, Adrian Sampson. +# Copyright 2011, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -50,6 +50,7 @@ DEFAULT_IMPORT_RESUME = None # "ask" DEFAULT_THREADED = True DEFAULT_COLOR = True +QUEUE_SIZE = 128 VARIOUS_ARTISTS = u'Various Artists' class ImportAbort(Exception): @@ -394,6 +395,7 @@ def initial_lookup(): is found, all of the yielded parameters (except items) are None. """ toppath, path, items = yield + log.debug('Looking up: %s' % path) while True: if path is DONE_SENTINEL: cur_artist, cur_album, candidates, rec = None, None, None, None @@ -610,7 +612,7 @@ def import_files(lib, paths, copy, write, autot, logpath, art, threaded, # Run the pipeline. try: if threaded: - pl.run_parallel() + pl.run_parallel(QUEUE_SIZE) else: pl.run_sequential() except ImportAbort: