Files

74 lines
2.4 KiB
Python

"""Small versions of cover art. Embedded covers can be several megabytes,
and everything shown goes through Flet's connection, so the album grid and
play bar get thumbnails, cached in ~/.cache/SaraSuite/SaraTune/Covers/."""
import hashlib
import io
from pathlib import Path
from typing import Optional
from PIL import Image, UnidentifiedImageError
from Core import CacheDir
from Metadata import ReadCover
CoversDir = CacheDir / "Covers"
_NoCover = b"" # remembered in the memory cache so a coverless album isn't read again
def Thumbnail(Data: bytes, Size: int) -> Optional[bytes]:
try:
with Image.open(io.BytesIO(Data)) as Picture:
Picture = Picture.convert("RGB")
Picture.thumbnail((Size, Size), Image.LANCZOS)
Output = io.BytesIO()
Picture.save(Output, "JPEG", quality=88)
return Output.getvalue()
except (UnidentifiedImageError, OSError, ValueError, Image.DecompressionBombError):
return None
_Memory: dict[tuple[str, int], bytes] = {}
def _CachePath(FilePath: str, Size: int) -> Optional[Path]:
try:
Stamp = Path(FilePath).stat().st_mtime_ns
except OSError:
return None
Name = hashlib.sha1(f"{FilePath}|{Size}|{Stamp}".encode("utf-8", "surrogateescape")).hexdigest()
return CoversDir / f"{Name}.jpg"
def CoverPath(FilePath: str, Size: int) -> Optional[Path]:
"""The cached thumbnail's file (made if needed), for the system media
controls, which want a picture file rather than bytes. Blocking."""
if not CoverFor(FilePath, Size):
return None
CachePath = _CachePath(FilePath, Size)
return CachePath if CachePath and CachePath.exists() else None
def CoverFor(FilePath: str, Size: int) -> Optional[bytes]:
"""FilePath's cover at most Size pixels wide. Blocking (run it in a
thread). Cached on disk by path, size and the file's mtime."""
Key = (FilePath, Size)
if Key in _Memory:
return _Memory[Key] or None
CachePath = _CachePath(FilePath, Size)
if CachePath is None:
return None
if CachePath.exists():
Data = CachePath.read_bytes()
else:
Original = ReadCover(FilePath)
Data = Thumbnail(Original, Size) if Original else None
if Data:
try:
CoversDir.mkdir(parents=True, exist_ok=True)
CachePath.write_bytes(Data)
except OSError:
pass
_Memory[Key] = Data or _NoCover
return Data