mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 13:07:09 +01:00
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:
parent
a06c278a20
commit
e99df7bc65
1 changed files with 59 additions and 12 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue