mirror of
https://github.com/beetbox/beets.git
synced 2025-12-16 05:34:47 +01:00
fetchart: add option to force cover format
This commit is contained in:
parent
8fb1c03ca5
commit
0b578a3384
2 changed files with 132 additions and 2 deletions
|
|
@ -18,6 +18,7 @@ public resizing proxy if neither is available.
|
|||
|
||||
import subprocess
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlencode
|
||||
|
|
@ -234,6 +235,72 @@ DEINTERLACE_FUNCS = {
|
|||
}
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -318,12 +385,50 @@ class ArtResizer(metaclass=Shareable):
|
|||
"""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"""
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class Candidate:
|
|||
CANDIDATE_DOWNSCALE = 2
|
||||
CANDIDATE_DOWNSIZE = 3
|
||||
CANDIDATE_DEINTERLACE = 4
|
||||
CANDIDATE_REFORMAT = 5
|
||||
|
||||
MATCH_EXACT = 0
|
||||
MATCH_FALLBACK = 1
|
||||
|
|
@ -74,12 +75,14 @@ class Candidate:
|
|||
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
|
||||
|
|
@ -142,12 +145,23 @@ class Candidate:
|
|||
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
|
||||
|
||||
|
|
@ -169,6 +183,12 @@ class Candidate:
|
|||
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):
|
||||
|
|
@ -923,6 +943,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
|
|||
'store_source': False,
|
||||
'high_resolution': False,
|
||||
'deinterlace': False,
|
||||
'cover_format': None,
|
||||
})
|
||||
self.config['google_key'].redact = True
|
||||
self.config['fanarttv_key'].redact = True
|
||||
|
|
@ -959,6 +980,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
|
|||
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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue