mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
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:
parent
56d9d9670f
commit
1fda7b6111
6 changed files with 90 additions and 26 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue