Skip to content

Commit

Permalink
Merge pull request #4133 from ArsenArsen/convert-cover-format
Browse files Browse the repository at this point in the history
Add option to convert cover art file format.
  • Loading branch information
sampsyo authored Nov 3, 2021
2 parents 8fb1c03 + 3de6574 commit 3b53181
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 2 deletions.
107 changes: 106 additions & 1 deletion beets/util/artresizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import subprocess
import os
import os.path
import re
from tempfile import NamedTemporaryFile
from urllib.parse import urlencode
Expand Down Expand Up @@ -234,6 +235,72 @@ def im_deinterlace(path_in, path_out=None):
}


def im_get_format(filepath):
cmd = ArtResizer.shared.im_identify_cmd + [
'-format', '%[magick]',
util.syspath(filepath)
]

try:
return util.command_output(cmd).stdout
except subprocess.CalledProcessError:
return None


def pil_get_format(filepath):
from PIL import Image, UnidentifiedImageError

try:
with Image.open(util.syspath(filepath)) as im:
return im.format
except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError):
log.exception("failed to detect image format for {}", filepath)
return None


BACKEND_GET_FORMAT = {
PIL: pil_get_format,
IMAGEMAGICK: im_get_format,
}


def im_convert_format(source, target, deinterlaced):
cmd = ArtResizer.shared.im_convert_cmd + [
util.syspath(source),
*(["-interlace", "none"] if deinterlaced else []),
util.syspath(target),
]

try:
subprocess.check_call(
cmd,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL
)
return target
except subprocess.CalledProcessError:
return source


def pil_convert_format(source, target, deinterlaced):
from PIL import Image, UnidentifiedImageError

try:
with Image.open(util.syspath(source)) as im:
im.save(util.py3_path(target), progressive=not deinterlaced)
return target
except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError,
OSError):
log.exception("failed to convert image {} -> {}", source, target)
return source


BACKEND_CONVERT_IMAGE_FORMAT = {
PIL: pil_convert_format,
IMAGEMAGICK: im_convert_format,
}


class Shareable(type):
"""A pseudo-singleton metaclass that allows both shared and
non-shared instances. The ``MyClass.shared`` property holds a
Expand Down Expand Up @@ -318,12 +385,50 @@ def get_size(self, path_in):
"""Return the size of an image file as an int couple (width, height)
in pixels.
Only available locally
Only available locally.
"""
if self.local:
func = BACKEND_GET_SIZE[self.method[0]]
return func(path_in)

def get_format(self, path_in):
"""Returns the format of the image as a string.
Only available locally.
"""
if self.local:
func = BACKEND_GET_FORMAT[self.method[0]]
return func(path_in)

def reformat(self, path_in, new_format, deinterlaced=True):
"""Converts image to desired format, updating its extension, but
keeping the same filename.
Only available locally.
"""
if not self.local:
return path_in

new_format = new_format.lower()
# A nonexhaustive map of image "types" to extensions overrides
new_format = {
'jpeg': 'jpg',
}.get(new_format, new_format)

fname, ext = os.path.splitext(path_in)
path_new = fname + b'.' + new_format.encode('utf8')
func = BACKEND_CONVERT_IMAGE_FORMAT[self.method[0]]

# allows the exception to propagate, while still making sure a changed
# file path was removed
result_path = path_in
try:
result_path = func(path_in, path_new, deinterlaced)
finally:
if result_path != path_in:
os.unlink(path_in)
return result_path

def _can_compare(self):
"""A boolean indicating whether image comparison is available"""

Expand Down
27 changes: 26 additions & 1 deletion beetsplug/fetchart.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Candidate:
CANDIDATE_DOWNSCALE = 2
CANDIDATE_DOWNSIZE = 3
CANDIDATE_DEINTERLACE = 4
CANDIDATE_REFORMAT = 5

MATCH_EXACT = 0
MATCH_FALLBACK = 1
Expand All @@ -74,12 +75,14 @@ def _validate(self, plugin):
Return `CANDIDATE_DOWNSIZE` if the file must be resized, and possibly
also rescaled.
Return `CANDIDATE_DEINTERLACE` if the file must be deinterlaced.
Return `CANDIDATE_REFORMAT` if the file has to be converted.
"""
if not self.path:
return self.CANDIDATE_BAD

if (not (plugin.enforce_ratio or plugin.minwidth or plugin.maxwidth
or plugin.max_filesize or plugin.deinterlace)):
or plugin.max_filesize or plugin.deinterlace
or plugin.cover_format)):
return self.CANDIDATE_EXACT

# get_size returns None if no local imaging backend is available
Expand Down Expand Up @@ -142,12 +145,23 @@ def _validate(self, plugin):
filesize, plugin.max_filesize)
downsize = True

# Check image format
reformat = False
if plugin.cover_format:
fmt = ArtResizer.shared.get_format(self.path)
reformat = fmt != plugin.cover_format
if reformat:
self._log.debug('image needs reformatting: {} -> {}',
fmt, plugin.cover_format)

if downscale:
return self.CANDIDATE_DOWNSCALE
elif downsize:
return self.CANDIDATE_DOWNSIZE
elif plugin.deinterlace:
return self.CANDIDATE_DEINTERLACE
elif reformat:
return self.CANDIDATE_REFORMAT
else:
return self.CANDIDATE_EXACT

Expand All @@ -169,6 +183,12 @@ def resize(self, plugin):
max_filesize=plugin.max_filesize)
elif self.check == self.CANDIDATE_DEINTERLACE:
self.path = ArtResizer.shared.deinterlace(self.path)
elif self.check == self.CANDIDATE_REFORMAT:
self.path = ArtResizer.shared.reformat(
self.path,
plugin.cover_format,
deinterlaced=plugin.deinterlace,
)


def _logged_get(log, *args, **kwargs):
Expand Down Expand Up @@ -923,6 +943,7 @@ def __init__(self):
'store_source': False,
'high_resolution': False,
'deinterlace': False,
'cover_format': None,
})
self.config['google_key'].redact = True
self.config['fanarttv_key'].redact = True
Expand Down Expand Up @@ -959,6 +980,10 @@ def __init__(self):
self.src_removed = (config['import']['delete'].get(bool) or
config['import']['move'].get(bool))

self.cover_format = self.config['cover_format'].get(
confuse.Optional(str)
)

if self.config['auto']:
# Enable two import hooks when fetching is enabled.
self.import_stages = [self.fetch_art]
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Other new things:
* :doc:`/plugins/fetchart`: A new option to store cover art as non-progressive
image. Useful for DAPs that support progressive images. Set ``deinterlace:
yes`` in your configuration to enable.
* :doc:`/plugins/fetchart`: A new option to change cover art format. Useful for
DAPs that do not support some image formats.

For plugin developers:

Expand Down
10 changes: 10 additions & 0 deletions docs/plugins/fetchart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ file. The available options are:
instructed to store cover art as non-progressive JPEG. You might need this if
you use DAPs that don't support progressive images.
Default: ``no``.
- **cover_format**: If enabled, forced the cover image into the specified
format. Most often, this will be either ``JPEG`` or ``PNG`` [#imgformats]_.
Also respects ``deinterlace``.
Default: None (leave unchanged).

Note: ``maxwidth`` and ``enforce_ratio`` options require either `ImageMagick`_
or `Pillow`_.
Expand All @@ -105,6 +109,12 @@ or `Pillow`_.
.. _beets custom search engine: https://cse.google.com.au:443/cse/publicurl?cx=001442825323518660753:hrh5ch1gjzm
.. _Pillow: https://github.com/python-pillow/Pillow
.. _ImageMagick: https://www.imagemagick.org/
.. [#imgformats] Other image formats are available, though the full list
depends on your system and what backend you are using. If you're using the
ImageMagick backend, you can use ``magick identify -list format`` to get a
full list of all supported formats, and you can use the Python function
PIL.features.pilinfo() to print a list of all supported formats in Pillow
(``python3 -c 'import PIL.features as f; f.pilinfo()'``).
Here's an example that makes plugin select only images that contain ``front`` or
``back`` keywords in their filenames and prioritizes the iTunes source over
Expand Down

0 comments on commit 3b53181

Please sign in to comment.