mirror of
https://git.srhtnin.garden/sarah/SaraTune.git
synced 2026-10-13 18:18:11 +02:00
74 lines
2.4 KiB
Python
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
|