fetchart, artresizer: Create art files in predictable directories

This allows to clean them up in art (1) fetching, (2) resizing and (3)
deinterlace tests.
This commit is contained in:
Šarūnas Nejus 2024-07-02 02:00:15 +01:00
parent 56d9d9670f
commit 1fda7b6111
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
6 changed files with 90 additions and 26 deletions

View file

@ -29,6 +29,7 @@ information or mock the environment.
- The `TestHelper` class encapsulates various fixtures that can be set up.
"""
from __future__ import annotations
import os
import os.path
@ -39,6 +40,7 @@ from contextlib import contextmanager
from enum import Enum
from io import StringIO
from tempfile import mkdtemp, mkstemp
from typing import ClassVar
import responses
from mediafile import Image, MediaFile
@ -50,7 +52,12 @@ from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Album, Item, Library
from beets.test import _common
from beets.ui.commands import TerminalImportSession
from beets.util import MoveOperation, bytestring_path, syspath
from beets.util import (
MoveOperation,
bytestring_path,
clean_module_tempdir,
syspath,
)
class LogCapture(logging.Handler):
@ -952,3 +959,13 @@ class FetchImageHelper:
# imghdr reads 32 bytes
body=self.IMAGEHEADER.get(file_type, b"").ljust(32, b"\x00"),
)
class CleanupModulesMixin:
modules: ClassVar[tuple[str, ...]]
@classmethod
def tearDownClass(cls) -> None:
"""Remove files created by the plugin."""
for module in cls.modules:
clean_module_tempdir(module)

View file

@ -13,6 +13,7 @@
# included in all copies or substantial portions of the Software.
"""Miscellaneous utility functions."""
from __future__ import annotations
import errno
import fnmatch
@ -26,9 +27,11 @@ import sys
import tempfile
import traceback
from collections import Counter, namedtuple
from contextlib import suppress
from enum import Enum
from logging import Logger
from multiprocessing.pool import ThreadPool
from pathlib import Path
from typing import (
Any,
AnyStr,
@ -58,6 +61,7 @@ MAX_FILENAME_LENGTH = 200
WINDOWS_MAGIC_PREFIX = "\\\\?\\"
T = TypeVar("T")
Bytes_or_String: TypeAlias = Union[str, bytes]
PathLike = Union[str, bytes, Path]
class HumanReadableException(Exception):
@ -1076,3 +1080,46 @@ class cached_classproperty: # noqa: N801
self.cache[owner] = self.getter(owner)
return self.cache[owner]
def get_module_tempdir(module: str) -> Path:
"""Return the temporary directory for the given module.
The directory is created within the `/tmp/beets/<module>` directory on
Linux (or the equivalent temporary directory on other systems).
Dots in the module name are replaced by underscores.
"""
module = module.replace("beets.", "").replace(".", "_")
return Path(tempfile.gettempdir()) / "beets" / module
def clean_module_tempdir(module: str) -> None:
"""Clean the temporary directory for the given module."""
tempdir = get_module_tempdir(module)
shutil.rmtree(tempdir, ignore_errors=True)
with suppress(OSError):
# remove parent (/tmp/beets) directory if it is empty
tempdir.parent.rmdir()
def get_temp_filename(
module: str,
prefix: str = "",
path: PathLike | None = None,
suffix: str = "",
) -> bytes:
"""Return temporary filename for the given module and prefix.
The filename starts with the given `prefix`.
If 'suffix' is given, it is used a the file extension.
If 'path' is given, we use the same suffix.
"""
if not suffix and path:
suffix = Path(os.fsdecode(path)).suffix
tempdir = get_module_tempdir(module)
tempdir.mkdir(parents=True, exist_ok=True)
_, filename = tempfile.mkstemp(dir=tempdir, prefix=prefix, suffix=suffix)
return bytestring_path(filename)

View file

@ -22,11 +22,10 @@ import platform
import re
import subprocess
from itertools import chain
from tempfile import NamedTemporaryFile
from urllib.parse import urlencode
from beets import logging, util
from beets.util import bytestring_path, displayable_path, syspath
from beets.util import displayable_path, get_temp_filename, syspath
PROXY_URL = "https://images.weserv.nl/"
@ -48,15 +47,6 @@ def resize_url(url, maxwidth, quality=0):
return "{}?{}".format(PROXY_URL, urlencode(params))
def temp_file_for(path):
"""Return an unused filename with the same extension as the
specified path.
"""
ext = os.path.splitext(path)[1]
with NamedTemporaryFile(suffix=os.fsdecode(ext), delete=False) as f:
return bytestring_path(f.name)
class LocalBackendNotAvailableError(Exception):
pass
@ -141,7 +131,9 @@ class IMBackend(LocalBackend):
Use the ``magick`` program or ``convert`` on older versions. Return
the output path of resized image.
"""
path_out = path_out or temp_file_for(path_in)
if not path_out:
path_out = get_temp_filename(__name__, "resize_IM_", path_in)
log.debug(
"artresizer: ImageMagick resizing {0} to {1}",
displayable_path(path_in),
@ -208,7 +200,8 @@ class IMBackend(LocalBackend):
return None
def deinterlace(self, path_in, path_out=None):
path_out = path_out or temp_file_for(path_in)
if not path_out:
path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in)
cmd = self.convert_cmd + [
syspath(path_in, prefix=False),
@ -366,7 +359,9 @@ class PILBackend(LocalBackend):
"""Resize using Python Imaging Library (PIL). Return the output path
of resized image.
"""
path_out = path_out or temp_file_for(path_in)
if not path_out:
path_out = get_temp_filename(__name__, "resize_PIL_", path_in)
from PIL import Image
log.debug(
@ -442,7 +437,9 @@ class PILBackend(LocalBackend):
return None
def deinterlace(self, path_in, path_out=None):
path_out = path_out or temp_file_for(path_in)
if not path_out:
path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in)
from PIL import Image
try:

View file

@ -19,14 +19,13 @@ import os
import re
from collections import OrderedDict
from contextlib import closing
from tempfile import NamedTemporaryFile
import confuse
import requests
from mediafile import image_mime_type
from beets import config, importer, plugins, ui, util
from beets.util import bytestring_path, sorted_walk, syspath
from beets.util import bytestring_path, get_temp_filename, sorted_walk, syspath
from beets.util.artresizer import ArtResizer
try:
@ -412,17 +411,17 @@ class RemoteArtSource(ArtSource):
ext,
)
suffix = os.fsdecode(ext)
with NamedTemporaryFile(suffix=suffix, delete=False) as fh:
filename = get_temp_filename(__name__, suffix=ext.decode())
with open(filename, "wb") as fh:
# write the first already loaded part of the image
fh.write(header)
# download the remaining part of the image
for chunk in data:
fh.write(chunk)
self._log.debug(
"downloaded art to: {0}", util.displayable_path(fh.name)
"downloaded art to: {0}", util.displayable_path(filename)
)
candidate.path = util.bytestring_path(fh.name)
candidate.path = util.bytestring_path(filename)
return
except (OSError, requests.RequestException, TypeError) as exc:

View file

@ -26,7 +26,7 @@ import responses
from beets import config, importer, library, logging, util
from beets.autotag import AlbumInfo, AlbumMatch
from beets.test import _common
from beets.test.helper import FetchImageHelper, capture_log
from beets.test.helper import CleanupModulesMixin, FetchImageHelper, capture_log
from beets.util import syspath
from beets.util.artresizer import ArtResizer
from beetsplug import fetchart
@ -44,7 +44,9 @@ class Settings:
setattr(self, k, v)
class UseThePlugin(_common.TestCase):
class UseThePlugin(CleanupModulesMixin, _common.TestCase):
modules = (fetchart.__name__, ArtResizer.__module__)
def setUp(self):
super().setUp()
self.plugin = fetchart.FetchArtPlugin()

View file

@ -20,7 +20,7 @@ import unittest
from unittest.mock import patch
from beets.test import _common
from beets.test.helper import TestHelper
from beets.test.helper import CleanupModulesMixin, TestHelper
from beets.util import command_output, syspath
from beets.util.artresizer import IMBackend, PILBackend
@ -48,9 +48,11 @@ class DummyPILBackend(PILBackend):
pass
class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
class ArtResizerFileSizeTest(CleanupModulesMixin, _common.TestCase, TestHelper):
"""Unittest test case for Art Resizer to a specific filesize."""
modules = (IMBackend.__module__,)
IMG_225x225 = os.path.join(_common.RSRC, b"abbey.jpg")
IMG_225x225_SIZE = os.stat(syspath(IMG_225x225)).st_size