mirror of
https://github.com/beetbox/beets.git
synced 2026-02-26 09:11:32 +01:00
modularized album tagger; all CLI I/O is now in bts
This commit is contained in:
parent
749b55b782
commit
de3bd2692f
2 changed files with 140 additions and 91 deletions
|
|
@ -19,9 +19,7 @@
|
|||
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from beets.autotag.mb import match_album
|
||||
from beets import library
|
||||
from beets.mediafile import FileTypeError
|
||||
from beets.autotag import mb
|
||||
|
||||
# If the MusicBrainz length is more than this many seconds away from the
|
||||
# track length, an error is reported. 30 seconds may seem like overkill,
|
||||
|
|
@ -29,7 +27,10 @@ from beets.mediafile import FileTypeError
|
|||
# threshold used by Picard before it even applies a penalty.
|
||||
LENGTH_TOLERANCE = 30
|
||||
|
||||
def likely_metadata(items):
|
||||
class AutotagError(Exception): pass
|
||||
class UnorderedTracksError(AutotagError): pass
|
||||
|
||||
def current_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.
|
||||
"""
|
||||
|
|
@ -62,20 +63,9 @@ def likely_metadata(items):
|
|||
|
||||
return (likelies['artist'], likelies['album'])
|
||||
|
||||
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].lower() == 'y':
|
||||
return True
|
||||
elif len(resp) > 0 and resp[0].lower() == 'n':
|
||||
return False
|
||||
resp = raw_input("Type 'y' or 'n': ")
|
||||
|
||||
def order_items(items):
|
||||
"""Given a list of items, put them in album order.
|
||||
"""
|
||||
# First, see if the current tags indicate an ordering.
|
||||
ordered_items = [None]*len(items)
|
||||
available_indices = set(range(len(items)))
|
||||
|
|
@ -100,69 +90,48 @@ def order_items(items):
|
|||
|
||||
return ordered_items
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
#fixme Check if MB tags are already present.
|
||||
# These accumulate the possible distance components. The final
|
||||
# distance will be dist/dist_max.
|
||||
dist = 0.0
|
||||
dist_max = 0.0
|
||||
|
||||
# Find existing metadata.
|
||||
cur_artist, cur_album = likely_metadata(items)
|
||||
# If either tag is missing, change should be confirmed.
|
||||
if len(cur_artist) == 0 or len(cur_album) == 0:
|
||||
return 1.0
|
||||
|
||||
# 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
|
||||
# Check whether the new values differ from the old ones.
|
||||
#fixme edit distance instead of 1/0
|
||||
#fixme filter non-alphanum
|
||||
if cur_artist.lower() != info['artist'].lower() or \
|
||||
cur_album.lower() != info['album'].lower():
|
||||
dist += 1.0
|
||||
dist_max += 1.0
|
||||
|
||||
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.
|
||||
ordered_items = order_items(items)
|
||||
if not ordered_items:
|
||||
print "Tracks could not be ordered."
|
||||
return
|
||||
|
||||
# Apply new metadata.
|
||||
for index, (item, track_data) in enumerate(zip(ordered_items,
|
||||
info['tracks']
|
||||
)):
|
||||
|
||||
# For safety, ensure track lengths match.
|
||||
# Find track distances.
|
||||
for item, track_data in zip(items, info['tracks']):
|
||||
# Check track length.
|
||||
if abs(item.length - track_data['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'])
|
||||
|
||||
# Abort with maximum. (fixme, something softer?)
|
||||
return 1.0
|
||||
#fixme track name
|
||||
|
||||
# Normalize distance, avoiding divide-by-zero.
|
||||
if dist_max == 0.0:
|
||||
return 0.0
|
||||
else:
|
||||
return dist/dist_max
|
||||
|
||||
def apply_metadata(items, info):
|
||||
"""Set the items' metadata to match the data given in info. The
|
||||
list of items must be ordered.
|
||||
"""
|
||||
for index, (item, track_data) in enumerate(zip(items, info['tracks'])):
|
||||
item.artist = info['artist']
|
||||
item.album = info['album']
|
||||
item.tracktotal = len(items)
|
||||
|
|
@ -176,19 +145,30 @@ def tag_album_dir(path, lib):
|
|||
item.title = track_data['title']
|
||||
item.track = index + 1
|
||||
|
||||
#fixme Set MusicBrainz IDs!
|
||||
#fixme Set MusicBrainz IDs
|
||||
|
||||
def tag_album(items):
|
||||
"""Bundles together the functionality used to infer tags for a
|
||||
set of items comprised by an album. Returns everything relevant
|
||||
and a little bit more:
|
||||
- The list of items, possibly reordered.
|
||||
- The current metadata: an (artist, album) tuple.
|
||||
- The inferred metadata dictionary.
|
||||
- The distance between the current and new metadata.
|
||||
May raise an UnorderedTracksError if existing metadata is
|
||||
insufficient.
|
||||
"""
|
||||
# Get current and candidate metadata.
|
||||
cur_artist, cur_album = current_metadata(items)
|
||||
info = mb.match_album(cur_artist, cur_album, len(items))
|
||||
|
||||
# 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()
|
||||
# Put items in order.
|
||||
items = order_items(items)
|
||||
if not items:
|
||||
raise UnorderedTracksError()
|
||||
|
||||
# Get the change distance.
|
||||
dist = distance(items, info)
|
||||
|
||||
return items, (cur_artist, cur_album), info, dist
|
||||
|
||||
|
|
|
|||
73
bts
73
bts
|
|
@ -18,10 +18,13 @@
|
|||
|
||||
from optparse import OptionParser
|
||||
from beets import Library
|
||||
from beets import autotag
|
||||
from ConfigParser import SafeConfigParser
|
||||
import os
|
||||
|
||||
from beets import autotag
|
||||
from beets import library
|
||||
from beets.mediafile import FileTypeError
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
# beets
|
||||
'library': 'library.blb',
|
||||
|
|
@ -38,6 +41,72 @@ def _print(txt):
|
|||
"""Print the text encoded using UTF-8."""
|
||||
print txt.encode('utf-8')
|
||||
|
||||
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].lower() == 'y':
|
||||
return True
|
||||
elif len(resp) > 0 and resp[0].lower() == '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 = os.path.join(path, filename)
|
||||
try:
|
||||
i = library.Item.from_path(filepath, lib)
|
||||
except FileTypeError:
|
||||
continue
|
||||
items.append(i)
|
||||
|
||||
# Infer tags.
|
||||
try:
|
||||
items, (cur_artist, cur_album), info, dist = autotag.tag_album(items)
|
||||
except autotag.UnorderedTracksError:
|
||||
print "Tracks could not be ordered."
|
||||
return
|
||||
|
||||
# Show what we're about to do.
|
||||
if cur_artist != info['artist'] or cur_album != info['album']:
|
||||
print "Correcting tags from:"
|
||||
print ' %s - %s' % (cur_artist, cur_album)
|
||||
print "To:"
|
||||
print ' %s - %s' % (info['artist'], info['album'])
|
||||
else:
|
||||
print "Tagging: %s - %s" % (info['artist'], info['album'])
|
||||
for item, track_data in zip(items, info['tracks']):
|
||||
if item.title != track_data['title']:
|
||||
print " %s -> %s" % (item.title, track_data['title'])
|
||||
|
||||
# Warn if change is significant.
|
||||
if dist > 0.0:
|
||||
if not _input_yn("Apply change ([y]/n)? "):
|
||||
return
|
||||
|
||||
# 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
|
||||
|
||||
# Change metadata and add to library.
|
||||
autotag.apply_metadata(items, info)
|
||||
for item in items:
|
||||
item.move(True)
|
||||
item.add()
|
||||
item.write()
|
||||
|
||||
|
||||
def add(lib, config, paths):
|
||||
for path in paths:
|
||||
lib.add(path)
|
||||
|
|
@ -96,7 +165,7 @@ def read(lib, config, criteria):
|
|||
|
||||
def tagalbum(lib, config, paths):
|
||||
for path in paths:
|
||||
autotag.tag_album_dir(os.path.expanduser(path), lib)
|
||||
tag_album_dir(os.path.expanduser(path), lib)
|
||||
lib.save()
|
||||
|
||||
def bpd(lib, config, opts):
|
||||
|
|
|
|||
Loading…
Reference in a new issue