mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
artresizer: type module
This commit is contained in:
parent
0379f68aea
commit
996a116a62
1 changed files with 89 additions and 34 deletions
|
|
@ -22,6 +22,8 @@ import platform
|
|||
import re
|
||||
import subprocess
|
||||
from itertools import chain
|
||||
from typing import AnyStr, Tuple, Optional, Mapping, Union
|
||||
from PIL import Image
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from beets import logging, util
|
||||
|
|
@ -32,7 +34,7 @@ PROXY_URL = "https://images.weserv.nl/"
|
|||
log = logging.getLogger("beets")
|
||||
|
||||
|
||||
def resize_url(url, maxwidth, quality=0):
|
||||
def resize_url(url: str, maxwidth: int, quality: int = 0) -> str:
|
||||
"""Return a proxied image URL that resizes the original image to
|
||||
maxwidth (preserving aspect ratio).
|
||||
"""
|
||||
|
|
@ -124,8 +126,13 @@ class IMBackend(LocalBackend):
|
|||
self.compare_cmd = ["magick", "compare"]
|
||||
|
||||
def resize(
|
||||
self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0
|
||||
):
|
||||
self,
|
||||
maxwidth: int,
|
||||
path_in: AnyStr,
|
||||
path_out: Optional[AnyStr] = None,
|
||||
quality: int = 0,
|
||||
max_filesize: int = 0,
|
||||
) -> AnyStr:
|
||||
"""Resize using ImageMagick.
|
||||
|
||||
Use the ``magick`` program or ``convert`` on older versions. Return
|
||||
|
|
@ -174,7 +181,7 @@ class IMBackend(LocalBackend):
|
|||
|
||||
return path_out
|
||||
|
||||
def get_size(self, path_in):
|
||||
def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]:
|
||||
cmd = self.identify_cmd + [
|
||||
"-format",
|
||||
"%w %h",
|
||||
|
|
@ -199,7 +206,7 @@ class IMBackend(LocalBackend):
|
|||
log.warning("Could not understand IM output: {0!r}", out)
|
||||
return None
|
||||
|
||||
def deinterlace(self, path_in, path_out=None):
|
||||
def deinterlace(self, path_in: AnyStr, path_out: Optional[AnyStr] = None) -> AnyStr:
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in)
|
||||
|
||||
|
|
@ -217,7 +224,7 @@ class IMBackend(LocalBackend):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return path_in
|
||||
|
||||
def get_format(self, filepath):
|
||||
def get_format(self, filepath: AnyStr) -> Optional[bytes]:
|
||||
cmd = self.identify_cmd + ["-format", "%[magick]", syspath(filepath)]
|
||||
|
||||
try:
|
||||
|
|
@ -226,7 +233,12 @@ class IMBackend(LocalBackend):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return None
|
||||
|
||||
def convert_format(self, source, target, deinterlaced):
|
||||
def convert_format(
|
||||
self,
|
||||
source: AnyStr,
|
||||
target: AnyStr,
|
||||
deinterlaced: bool,
|
||||
) -> AnyStr:
|
||||
cmd = self.convert_cmd + [
|
||||
syspath(source),
|
||||
*(["-interlace", "none"] if deinterlaced else []),
|
||||
|
|
@ -243,10 +255,15 @@ class IMBackend(LocalBackend):
|
|||
return source
|
||||
|
||||
@property
|
||||
def can_compare(self):
|
||||
def can_compare(self) -> bool:
|
||||
return self.version() > (6, 8, 7)
|
||||
|
||||
def compare(self, im1, im2, compare_threshold):
|
||||
def compare(
|
||||
self,
|
||||
im1: Image,
|
||||
im2: Image,
|
||||
compare_threshold: float,
|
||||
) -> Optional[bool]:
|
||||
is_windows = platform.system() == "Windows"
|
||||
|
||||
# Converting images to grayscale tends to minimize the weight
|
||||
|
|
@ -329,10 +346,10 @@ class IMBackend(LocalBackend):
|
|||
return phash_diff <= compare_threshold
|
||||
|
||||
@property
|
||||
def can_write_metadata(self):
|
||||
def can_write_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def write_metadata(self, file, metadata):
|
||||
def write_metadata(self, file: AnyStr, metadata: Mapping):
|
||||
assignments = list(
|
||||
chain.from_iterable(("-set", k, v) for k, v in metadata.items())
|
||||
)
|
||||
|
|
@ -359,8 +376,13 @@ class PILBackend(LocalBackend):
|
|||
self.version()
|
||||
|
||||
def resize(
|
||||
self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0
|
||||
):
|
||||
self,
|
||||
maxwidth: int,
|
||||
path_in: AnyStr,
|
||||
path_out: Optional[AnyStr] = None,
|
||||
quality: int = 0,
|
||||
max_filesize: int = 0,
|
||||
) -> AnyStr:
|
||||
"""Resize using Python Imaging Library (PIL). Return the output path
|
||||
of resized image.
|
||||
"""
|
||||
|
|
@ -429,7 +451,7 @@ class PILBackend(LocalBackend):
|
|||
)
|
||||
return path_in
|
||||
|
||||
def get_size(self, path_in):
|
||||
def get_size(self, path_in: AnyStr) -> Optional[Tuple[int, int]]:
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
|
|
@ -441,7 +463,11 @@ class PILBackend(LocalBackend):
|
|||
)
|
||||
return None
|
||||
|
||||
def deinterlace(self, path_in, path_out=None):
|
||||
def deinterlace(
|
||||
self,
|
||||
path_in: AnyStr,
|
||||
path_out: Optional[AnyStr] = None,
|
||||
) -> AnyStr:
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in)
|
||||
|
||||
|
|
@ -455,7 +481,7 @@ class PILBackend(LocalBackend):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return path_in
|
||||
|
||||
def get_format(self, filepath):
|
||||
def get_format(self, filepath: AnyStr) -> Optional[str]:
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
try:
|
||||
|
|
@ -470,7 +496,12 @@ class PILBackend(LocalBackend):
|
|||
log.exception("failed to detect image format for {}", filepath)
|
||||
return None
|
||||
|
||||
def convert_format(self, source, target, deinterlaced):
|
||||
def convert_format(
|
||||
self,
|
||||
source: AnyStr,
|
||||
target: AnyStr,
|
||||
deinterlaced: bool,
|
||||
) -> str:
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
try:
|
||||
|
|
@ -488,18 +519,23 @@ class PILBackend(LocalBackend):
|
|||
return source
|
||||
|
||||
@property
|
||||
def can_compare(self):
|
||||
def can_compare(self) -> bool:
|
||||
return False
|
||||
|
||||
def compare(self, im1, im2, compare_threshold):
|
||||
def compare(
|
||||
self,
|
||||
im1: Image,
|
||||
im2: Image,
|
||||
compare_threshold: float,
|
||||
):
|
||||
# It is an error to call this when ArtResizer.can_compare is not True.
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def can_write_metadata(self):
|
||||
def can_write_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def write_metadata(self, file, metadata):
|
||||
def write_metadata(self, file: AnyStr, metadata: Mapping):
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
# FIXME: Detect and handle other file types (currently, the only user
|
||||
|
|
@ -523,7 +559,7 @@ class Shareable(type):
|
|||
cls._instance = None
|
||||
|
||||
@property
|
||||
def shared(cls):
|
||||
def shared(cls) -> 'Shareable':
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
|
@ -554,14 +590,19 @@ class ArtResizer(metaclass=Shareable):
|
|||
self.local_method = None
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
def method(self) -> str:
|
||||
if self.local:
|
||||
return self.local_method.NAME
|
||||
else:
|
||||
return "WEBPROXY"
|
||||
|
||||
def resize(
|
||||
self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0
|
||||
self,
|
||||
maxwidth: int,
|
||||
path_in: AnyStr,
|
||||
path_out: Optional[AnyStr]=None,
|
||||
quality: int = 0,
|
||||
max_filesize: int = 0,
|
||||
):
|
||||
"""Manipulate an image file according to the method, returning a
|
||||
new path. For PIL or IMAGEMAGIC methods, resizes the image to a
|
||||
|
|
@ -580,7 +621,11 @@ class ArtResizer(metaclass=Shareable):
|
|||
# Handled by `proxy_url` already.
|
||||
return path_in
|
||||
|
||||
def deinterlace(self, path_in, path_out=None):
|
||||
def deinterlace(
|
||||
self,
|
||||
path_in: AnyStr,
|
||||
path_out: Optional[AnyStr] = None,
|
||||
) -> AnyStr:
|
||||
"""Deinterlace an image.
|
||||
|
||||
Only available locally.
|
||||
|
|
@ -591,7 +636,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return path_in
|
||||
|
||||
def proxy_url(self, maxwidth, url, quality=0):
|
||||
def proxy_url(self, maxwidth: int, url: str, quality: int = 0):
|
||||
"""Modifies an image URL according the method, returning a new
|
||||
URL. For WEBPROXY, a URL on the proxy server is returned.
|
||||
Otherwise, the URL is returned unmodified.
|
||||
|
|
@ -603,13 +648,13 @@ class ArtResizer(metaclass=Shareable):
|
|||
return resize_url(url, maxwidth, quality)
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
def local(self) -> bool:
|
||||
"""A boolean indicating whether the resizing method is performed
|
||||
locally (i.e., PIL or ImageMagick).
|
||||
"""
|
||||
return self.local_method is not None
|
||||
|
||||
def get_size(self, path_in):
|
||||
def get_size(self, path_in: AnyStr) -> Union[Tuple[int, int], AnyStr]:
|
||||
"""Return the size of an image file as an int couple (width, height)
|
||||
in pixels.
|
||||
|
||||
|
|
@ -621,7 +666,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return path_in
|
||||
|
||||
def get_format(self, path_in):
|
||||
def get_format(self, path_in: AnyStr) -> Optional[str]:
|
||||
"""Returns the format of the image as a string.
|
||||
|
||||
Only available locally.
|
||||
|
|
@ -632,7 +677,12 @@ class ArtResizer(metaclass=Shareable):
|
|||
# FIXME: Should probably issue a warning?
|
||||
return None
|
||||
|
||||
def reformat(self, path_in, new_format, deinterlaced=True):
|
||||
def reformat(
|
||||
self,
|
||||
path_in: AnyStr,
|
||||
new_format: str,
|
||||
deinterlaced: bool = True,
|
||||
) -> AnyStr:
|
||||
"""Converts image to desired format, updating its extension, but
|
||||
keeping the same filename.
|
||||
|
||||
|
|
@ -664,7 +714,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
return result_path
|
||||
|
||||
@property
|
||||
def can_compare(self):
|
||||
def can_compare(self) -> bool:
|
||||
"""A boolean indicating whether image comparison is available"""
|
||||
|
||||
if self.local:
|
||||
|
|
@ -672,7 +722,12 @@ class ArtResizer(metaclass=Shareable):
|
|||
else:
|
||||
return False
|
||||
|
||||
def compare(self, im1, im2, compare_threshold):
|
||||
def compare(
|
||||
self,
|
||||
im1: Image,
|
||||
im2: Image,
|
||||
compare_threshold: float,
|
||||
) -> Optional[bool]:
|
||||
"""Return a boolean indicating whether two images are similar.
|
||||
|
||||
Only available locally.
|
||||
|
|
@ -684,7 +739,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
return None
|
||||
|
||||
@property
|
||||
def can_write_metadata(self):
|
||||
def can_write_metadata(self) -> bool:
|
||||
"""A boolean indicating whether writing image metadata is supported."""
|
||||
|
||||
if self.local:
|
||||
|
|
@ -692,7 +747,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
else:
|
||||
return False
|
||||
|
||||
def write_metadata(self, file, metadata):
|
||||
def write_metadata(self, file: AnyStr, metadata: Mapping):
|
||||
"""Write key-value metadata to the image file.
|
||||
|
||||
Only available locally. Currently, expects the image to be a PNG file.
|
||||
|
|
|
|||
Loading…
Reference in a new issue