embedart: add compare_threshold option

if compare_threshold > 0 we call check_art_similarity to return sooner
if it happens that candidate image and embedded one are similar.
This commit is contained in:
Fabrice Laporte 2014-09-17 22:01:08 +02:00
parent a06c278a20
commit e99df7bc65

View file

@ -16,6 +16,8 @@
import os.path
import logging
import imghdr
import subprocess
from tempfile import NamedTemporaryFile
from beets.plugins import BeetsPlugin
from beets import mediafile
@ -23,7 +25,8 @@ from beets import ui
from beets.ui import decargs
from beets.util import syspath, normpath, displayable_path
from beets.util.artresizer import ArtResizer
from beets import config
from beets import config, util
log = logging.getLogger('beets')
@ -36,12 +39,17 @@ class EmbedCoverArtPlugin(BeetsPlugin):
self.config.add({
'maxwidth': 0,
'auto': True,
'compare_threshold': 0
})
if self.config['maxwidth'].get(int) and \
not ArtResizer.shared.local:
if self.config['maxwidth'].get(int) and not ArtResizer.shared.local:
self.config['maxwidth'] = 0
log.warn(u"embedart: ImageMagick or PIL not found; "
u"'maxwidth' option ignored")
if self.config['compare_threshold'].get(int) and \
not ArtResizer.shared.check_method(ArtResizer.IMAGEMAGICK):
self.config['compare_threshold'] = 0
log.warn(u"embedart: ImageMagick not found; "
u"'compare_threshold' option ignored")
def commands(self):
# Embed command.
@ -52,12 +60,14 @@ class EmbedCoverArtPlugin(BeetsPlugin):
'-f', '--file', metavar='PATH', help='the image file to embed'
)
maxwidth = config['embedart']['maxwidth'].get(int)
compare_threshold = config['embedart']['compare_threshold'].get(int)
def embed_func(lib, opts, args):
if opts.file:
imagepath = normpath(opts.file)
for item in lib.items(decargs(args)):
embed_item(item, imagepath, maxwidth)
embed_item(item, imagepath, maxwidth, None,
compare_threshold)
else:
for album in lib.albums(decargs(args)):
embed_album(album, maxwidth)
@ -72,7 +82,8 @@ class EmbedCoverArtPlugin(BeetsPlugin):
def extract_func(lib, opts, args):
outpath = normpath(opts.outpath or 'cover')
extract(lib, outpath, decargs(args))
query = lib.items(decargs(args)).get()
extract(outpath, query)
extract_cmd.func = extract_func
# Clear command.
@ -94,10 +105,16 @@ def album_imported(lib, album):
embed_album(album, config['embedart']['maxwidth'].get(int))
def embed_item(item, imagepath, maxwidth=None, itempath=None):
def embed_item(item, imagepath, maxwidth=None, itempath=None,
compare_threshold=0):
"""Embed an image into the item's media file.
"""
if compare_threshold:
if not check_art_similarity(item, imagepath, compare_threshold):
log.warn('Image not similar, skipping it.')
return
try:
log.info(u'embedart: writing %s', displayable_path(imagepath))
item['images'] = [_mediafile_image(imagepath, maxwidth)]
item.try_write(itempath)
except IOError as exc:
@ -124,7 +141,38 @@ def embed_album(album, maxwidth=None):
.format(album))
for item in album.items():
embed_item(item, imagepath, maxwidth)
embed_item(item, imagepath, maxwidth, None,
config['embedart']['compare_threshold'].get(int))
def check_art_similarity(item, imagepath, compare_threshold):
"""A boolean indicating if an image is similar to embedded item art.
"""
with NamedTemporaryFile(delete=True) as f:
art = extract(f.name, item)
if art:
# Converting images to grayscale tends to minimize the weight
# of colors in the diff score
cmd = 'convert {0} {1} -colorspace gray MIFF:- | ' \
'compare -metric PHASH - null:'.format(syspath(imagepath),
syspath(art))
try:
phashDiff = util.command_output(cmd, shell=True)
except subprocess.CalledProcessError, e:
if e.returncode != 1:
log.warn(u'embedart: IM phashes compare failed for {0}, \
{1}'.format(displayable_path(imagepath),
displayable_path(art)))
return
phashDiff = float(e.output)
log.info(u'embedart: compare PHASH score is {0}'.format(phashDiff))
if phashDiff > compare_threshold:
return False
return True
def _mediafile_image(image_path, maxwidth=None):
@ -142,8 +190,7 @@ def _mediafile_image(image_path, maxwidth=None):
# 'extractart' command.
def extract(lib, outpath, query):
item = lib.items(query).get()
def extract(outpath, item):
if not item:
log.error(u'No item matches query.')
return
@ -170,11 +217,11 @@ def extract(lib, outpath, query):
return
outpath += '.' + ext
log.info(u'Extracting album art from: {0.artist} - {0.title}\n'
u'To: {1}'.format(item, displayable_path(outpath)))
log.info(u'Extracting album art from: {0.artist} - {0.title} '
u'to: {1}'.format(item, displayable_path(outpath)))
with open(syspath(outpath), 'wb') as f:
f.write(art)
return outpath
# 'clearart' command.