"""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