This commit is contained in:
Adrian Sampson 2011-04-02 22:38:28 -07:00
commit 379b998a02
6 changed files with 119 additions and 16 deletions

1
NEWS
View file

@ -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.

View file

@ -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))

View file

@ -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)

View file

@ -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
@ -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
@ -51,6 +50,9 @@ DEFAULT_IMPORT_RESUME = None # "ask"
DEFAULT_THREADED = True
DEFAULT_COLOR = True
QUEUE_SIZE = 128
VARIOUS_ARTISTS = u'Various Artists'
class ImportAbort(Exception):
"""Raised when the user aborts the tagging operation.
"""
@ -77,16 +79,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))
@ -381,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
@ -597,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:

57
beetsplug/info.py Normal file
View file

@ -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]

View file

@ -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'):