Add 'maxwidth' option to embedart and fetchart plugins.

artresizer.py instances an ArtResizer object that uses internally the PIL; ImageMagick
or a web proxy service to perform the resizing operations.
Because embedart works on input images located on filesystem it requires PIL or ImageMagick, whereas
fetchart is able to do the job with the fallback webproxy resizer.
This commit is contained in:
Fabrice Laporte 2012-10-28 15:36:42 +01:00
parent 99e36d870e
commit 7f2aa44ac6
5 changed files with 314 additions and 101 deletions

172
beets/util/artresizer.py Normal file
View file

@ -0,0 +1,172 @@
# This file is part of beets.
# Copyright 2012, Fabrice Laporte
#
# 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.
import urllib
import subprocess
import os
import glob
import shutil
from tempfile import NamedTemporaryFile
import logging
"""Abstraction layer to resize an image without requiring additional dependency
"""
# Resizing methods
PIL = 1
IMAGEMAGICK = 2
WEBPROXY = 3
log = logging.getLogger('beets')
class ArtResizerError(Exception):
"""Raised when an error occurs during image resizing
"""
def call(args):
"""Execute the command indicated by `args` (a list of strings) and
return the command's output. The stderr stream is ignored. If the
command exits abnormally, a ArtResizerError is raised.
"""
try:
with open(os.devnull, 'w') as devnull:
return subprocess.check_output(args, stderr=devnull)
except subprocess.CalledProcessError as e:
raise ArtResizerError(
"{0} exited with status {1}".format(args[0], e.returncode)
)
def resize_url(url, maxwidth):
"""Return a new url with image of original url resized to maxwidth (keep
aspect ratio)"""
PROXY_URL = 'http://images.weserv.nl/?url=%s&w=%s'
reqUrl = PROXY_URL % (url.replace('http://',''), maxwidth)
log.debug("Requesting proxy image at %s" % reqUrl)
try:
urllib.urlopen(reqUrl)
except IOError:
log.info('Cannot get resized image via web proxy. '
'Using original image url.')
return url
return reqUrl
def get_temp_file_out(path_in):
"""Return an unused filename with correct extension.
"""
with NamedTemporaryFile(suffix=os.path.splitext(path_in)[1]) as f:
path_out = f.name
return path_out
class PilResizer(object):
def resize(self, maxwidth, path_in, path_out=None) :
"""Resize using Python Imaging Library (PIL). Return the output path
of resized image.
"""
if not path_out:
path_out = get_temp_file_out(path_in)
try:
im = Image.open(path_in)
size = maxwidth, maxwidth
im.thumbnail(size, Image.ANTIALIAS)
im.save(path_out)
return path_out
except IOError:
log.error("Cannot create thumbnail for '%s'" % path)
class ImageMagickResizer(object):
def resize(self, maxwidth, path_in, path_out=None):
"""Resize using ImageMagick <http://www.imagemagick.org> command-line
tool. Return the output path of resized image.
"""
if not path_out:
path_out = get_temp_file_out(path_in)
# widthxheight> Shrinks images with dimension(s) larger than the
# corresponding width and/or height dimension(s).
# "only shrink flag" is prefixed by ^ escape char for Windows compat.
cmd = [self.convert_path, path_in, '-resize', '%sx^>' % \
maxwidth, path_out]
call(cmd)
return path_out
class ArtResizer(object):
convert_path = None
def __init__(self, detect=True):
"""ArtResizer factory method"""
self.method = WEBPROXY
if detect:
self.method = self.set_method()
if self.method == PIL :
self.__class__ = PilResizer
elif self.method == IMAGEMAGICK :
self.__class__ = ImageMagickResizer
log.debug("ArtResizer method is %s" % self.__class__)
def set_method(self):
"""Set the most appropriate resize method. Use PIL if present, else
check if ImageMagick is installed.
If none is available, use a web proxy."""
try:
from PIL import Image
return PIL
except ImportError as e:
pass
for dir in os.environ['PATH'].split(os.pathsep):
if glob.glob(os.path.join(dir, 'convert*')):
convert = os.path.join(dir, 'convert')
cmd = [convert, '--version']
try:
out = subprocess.check_output(cmd).lower()
if 'imagemagick' in out:
self.convert_path = convert
return IMAGEMAGICK
except subprocess.CalledProcessError as e:
pass # system32/convert.exe may be interfering
return WEBPROXY
def resize(self, maxwidth, url, path_out=None):
"""Resize using web proxy. Return the output path of resized image.
"""
reqUrl = resize_url(url, maxwidth)
try:
fn, headers = urllib.urlretrieve(reqUrl)
except IOError:
log.debug('error fetching resized image')
return
if not path_out:
path_out = get_temp_file_out(fn)
shutil.copy(fn, path_out)
return path_out
# module-as-singleton instanciation
inst = ArtResizer()

View file

@ -20,13 +20,17 @@ from beets.plugins import BeetsPlugin
from beets import mediafile
from beets import ui
from beets.ui import decargs
from beets.util import syspath, normpath
from beets.util import syspath, normpath, artresizer
log = logging.getLogger('beets')
def _embed(path, items):
"""Embed an image file, located at `path`, into each item.
"""
if options['maxwidth']:
path = artresizer.inst.resize(options['maxwidth'], syspath(path))
log.debug('Resize album art to %s before embedding' % path)
data = open(syspath(path), 'rb').read()
kindstr = imghdr.what(None, data)
if kindstr not in ('jpeg', 'png'):
@ -35,6 +39,7 @@ def _embed(path, items):
# Add art to each file.
log.debug('Embedding album art.')
for item in items:
try:
f = mediafile.MediaFile(syspath(item.path))
@ -48,12 +53,21 @@ def _embed(path, items):
options = {
'autoembed': True,
'maxwidth': 0,
}
class EmbedCoverArtPlugin(BeetsPlugin):
"""Allows albumart to be embedded into the actual files."""
"""Allows albumart to be embedded into the actual files.
"""
def configure(self, config):
options['autoembed'] = \
ui.config_val(config, 'embedart', 'autoembed', True, bool)
options['maxwidth'] = \
int(ui.config_val(config, 'embedart', 'maxwidth', '0'))
if options['maxwidth'] and artresizer.inst.method == artresizer.WEBPROXY:
options['maxwidth'] = 0
log.error("embedart: 'maxwidth' option ignored, "
"please install ImageMagick first")
def commands(self):
# Embed command.

View file

@ -20,6 +20,7 @@ import logging
import os
from beets.plugins import BeetsPlugin
from beets.util import artresizer
from beets import importer
from beets import ui
@ -32,6 +33,15 @@ log = logging.getLogger('beets')
# ART SOURCES ################################################################
def do_resize_url(func):
def wrapper(url, maxwidth=None):
"""Returns url pointing to resized image instead of original one"""
if maxwidth and artresizer.inst.method == artresizer.WEBPROXY :
url = artresizer.resize_url(url, maxwidth)
return func(url)
return wrapper
@do_resize_url
def _fetch_image(url):
"""Downloads an image from a URL and checks whether it seems to
actually be an image. If so, returns a path to the downloaded image.
@ -57,11 +67,8 @@ def _fetch_image(url):
CAA_URL = 'http://coverartarchive.org/release/{mbid}/front-500.jpg'
def caa_art(release_id):
"""Fetch album art from the Cover Art Archive given a MusicBrainz
release ID.
"""
url = CAA_URL.format(mbid=release_id)
return _fetch_image(url)
"""Return a Cover Art Archive url given a MusicBrain release ID."""
return CAA_URL.format(mbid=release_id)
# Art from Amazon.
@ -70,13 +77,15 @@ AMAZON_URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg'
AMAZON_INDICES = (1, 2)
def art_for_asin(asin):
"""Fetch art for an Amazon ID (ASIN) string."""
"""Return url for an Amazon ID (ASIN) string."""
for index in AMAZON_INDICES:
# Fetch the image.
url = AMAZON_URL % (asin, index)
fn = _fetch_image(url)
if fn:
return fn
try:
urlopen(url)
return url
except IOError:
pass # does not exist
# AlbumArt.org scraper.
@ -85,7 +94,7 @@ AAO_URL = 'http://www.albumart.org/index_detail.php'
AAO_PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"'
def aao_art(asin):
"""Fetch art from AlbumArt.org."""
"""Return art url from AlbumArt.org."""
# Get the page from albumart.org.
url = '%s?%s' % (AAO_URL, urllib.urlencode({'asin': asin}))
try:
@ -99,7 +108,7 @@ def aao_art(asin):
m = re.search(AAO_PAT, page)
if m:
image_url = m.group(1)
return _fetch_image(image_url)
return image_url
else:
log.debug('No image found on page')
@ -117,11 +126,10 @@ def art_in_path(path):
for ext in IMAGE_EXTENSIONS:
if fn.lower().endswith('.' + ext):
images.append(fn)
images.sort()
# Look for "preferred" filenames.
for name in COVER_NAMES:
for fn in images:
for fn in images:
for name in COVER_NAMES:
if fn.lower().startswith(name):
log.debug('Using well-named art file %s' % fn)
return os.path.join(path, fn)
@ -133,40 +141,41 @@ def art_in_path(path):
# Try each source in turn.
def art_for_album(album, path, local_only=False):
def art_for_album(album, path, maxwidth=None, local_only=False):
"""Given an Album object, returns a path to downloaded art for the
album (or None if no art is found). If `local_only`, then only local
image files from the filesystem are returned; no network requests
are made.
"""
# Local art.
if isinstance(path, basestring):
out = art_in_path(path)
if out:
return out
if local_only:
# Abort without trying Web sources.
return
# CoverArtArchive.org.
if album.mb_albumid:
log.debug('Fetching album art for MBID {0}.'.format(album.mb_albumid))
out = caa_art(album.mb_albumid)
if out:
return out
if not local_only and not out:
url = None
# CoverArtArchive.org.
if album.mb_albumid:
log.debug('Fetching album art for MBID {0}.'.format(album.mb_albumid))
url = caa_art(album.mb_albumid)
# Amazon and AlbumArt.org.
if not url and album.asin:
log.debug('Fetching album art for ASIN %s.' % album.asin)
url = art_for_asin(album.asin)
if not url:
url = aao_art(album.asin)
# Amazon and AlbumArt.org.
if album.asin:
log.debug('Fetching album art for ASIN %s.' % album.asin)
out = art_for_asin(album.asin)
if out:
return out
return aao_art(album.asin)
if not url: # All sources failed.
log.debug('No ASIN available: no art found.')
return None
# All sources failed.
log.debug('No ASIN available: no art found.')
return None
out = _fetch_image(url, maxwidth)
if maxwidth and artresizer.inst.method != artresizer.WEBPROXY :
artresizer.inst.resize(maxwidth, out, out)
return out
# PLUGIN LOGIC ###############################################################
@ -179,7 +188,8 @@ def batch_fetch_art(lib, albums, force):
if album.artpath and not force:
message = 'has album art'
else:
path = art_for_album(album, None)
path = art_for_album(album)
if path:
album.set_art(path, False)
message = 'found album art'
@ -201,11 +211,14 @@ class FetchArtPlugin(BeetsPlugin):
def configure(self, config):
self.autofetch = ui.config_val(config, 'fetchart',
'autofetch', True, bool)
self.maxwidth = int(ui.config_val(config, 'fetchart',
'maxwidth', '0'))
if self.autofetch:
# Enable two import hooks when fetching is enabled.
self.import_stages = [self.fetch_art]
self.register_listener('import_task_files', self.assign_art)
# Asynchronous; after music is added to the library.
def fetch_art(self, config, task):
"""Find art for the album being imported."""
@ -221,7 +234,8 @@ class FetchArtPlugin(BeetsPlugin):
return
album = config.lib.get_album(task.album_id)
path = art_for_album(album, task.path, local_only=local)
path = art_for_album(album, task.path, self.maxwidth, local_only=local)
if path:
self.art_paths[task] = path

View file

@ -40,8 +40,14 @@ embedded album art:
Configuring
-----------
The plugin has one configuration option, ``autoembed``, which lets you disable
automatic album art embedding. To do so, add this to your ``~/.beetsconfig``::
``autoembed`` option lets you disable automatic album art embedding.
To do so, add this to your ``~/.beetsconfig``::
[embedart]
autoembed: no
A maximum image width can be defined to downscale images before embedding them
(source image on filesystem is not altered). The resize operation reduces image width to
``maxwidth`` pixels and height is recomputed so that aspect ratio is preserved.
The [PIL](http://www.pythonware.com/products/pil/) or [ImageMagick](www.imagemagick.org/) is required
to use the ``maxwidth`` config option.

View file

@ -1,57 +1,64 @@
FetchArt Plugin
===============
The ``fetchart`` plugin retrieves album art images from various sources on the
Web and stores them as image files.
Fetching Album Art During Import
--------------------------------
To automatically get album art for every album you import, just enable the
plugin by putting ``fetchart`` on your config file's ``plugins`` line (see
:doc:`/plugins/index`).
By default, beets stores album art image files alongside the music files for an
album in a file called ``cover.jpg``. To customize the name of this file, use
the :ref:`art-filename` config option.
To disable automatic art downloading, just put this in your configuration
file::
[fetchart]
autofetch: no
Manually Fetching Album Art
---------------------------
Use the ``fetchart`` command to download album art after albums have already
been imported::
$ beet fetchart [-f] [query]
By default, the command will only look for album art when the album doesn't
already have it; the ``-f`` or ``--force`` switch makes it search for art
regardless. If you specify a query, only matching albums will be processed;
otherwise, the command processes every album in your library.
Album Art Sources
-----------------
Currently, this plugin searches for art in the local filesystem as well as on
the Cover Art Archive, Amazon, and AlbumArt.org (in that order).
When looking for local album art, beets checks for image files located in the
same folder as the music files you're importing. If you have an image file
called "cover," "front," "art," "album," for "folder" alongside your music,
beets will treat it as album art and skip searching any online databases.
When you choose to apply changes during an import, beets searches all sources
for album art. For "as-is" imports (and non-autotagged imports using the ``-A``
flag), beets only looks for art on the local filesystem.
Embedding Album Art
-------------------
This plugin fetches album art but does not embed images into files' tags. To do
that, use the :doc:`/plugins/embedart`. (You'll want to have both plugins
enabled.)
FetchArt Plugin
===============
The ``fetchart`` plugin retrieves album art images from various sources on the
Web and stores them as image files.
Fetching Album Art During Import
--------------------------------
To automatically get album art for every album you import, just enable the
plugin by putting ``fetchart`` on your config file's ``plugins`` line (see
:doc:`/plugins/index`).
By default, beets stores album art image files alongside the music files for an
album in a file called ``cover.jpg``. To customize the name of this file, use
the :ref:`art-filename` config option.
A maximum image width can be defined to downscale fetched images if they are too
big. The resize operation reduces image width to ``maxwidth`` pixels and
height is recomputed so that aspect ratio is preserved.
When using ``maxwidth`` config option, please consider installing
[ImageMagick](www.imagemagick.org/) first for optimal performance.
To disable automatic art downloading, just put this in your configuration
file::
[fetchart]
autofetch: no
Manually Fetching Album Art
---------------------------
Use the ``fetchart`` command to download album art after albums have already
been imported::
$ beet fetchart [-f] [query]
By default, the command will only look for album art when the album doesn't
already have it; the ``-f`` or ``--force`` switch makes it search for art
regardless. If you specify a query, only matching albums will be processed;
otherwise, the command processes every album in your library.
Album Art Sources
-----------------
Currently, this plugin searches for art in the local filesystem as well as on
the Cover Art Archive, Amazon, and AlbumArt.org (in that order).
When looking for local album art, beets checks for image files located in the
same folder as the music files you're importing. If you have an image file
called "cover," "front," "art," "album," for "folder" alongside your music,
beets will treat it as album art and skip searching any online databases.
When you choose to apply changes during an import, beets searches all sources
for album art. For "as-is" imports (and non-autotagged imports using the ``-A``
flag), beets only looks for art on the local filesystem.
Embedding Album Art
-------------------
This plugin fetches album art but does not embed images into files' tags. To do
that, use the :doc:`/plugins/embedart`. (You'll want to have both plugins
enabled.)