- Rewrite LibraryCache from JSON (cache dir) to SQLite (data dir with WAL) - Each track gets a stable UUID so play stats survive file renames/re-encodes - Thread-safe via PRAGMA busy_timeout + RLock - Automatic backup rotation (.backup, .bak) on every write - One-shot migration from old ~/.cache/library_cache.json - Add SessionStore (~/.local/share/session.json) for playback state - Add SplashOverlay with spinning animation on first (empty-DB) launch - _scan_library() now accepts on_progress callback for the overlay - Fix _mtime → mtime SQLite column name lookup in recently-added filter - PlaylistManager path migrated to ~/.local/share/ with auto-migration - Metadata sync: remove dead consume_play_deltas(), use library API
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Local cover art cache for the player UI.
|
|
|
|
Caches extracted cover art as JPEG files in a local directory
|
|
so the player can display them without re-extracting from audio.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
|
|
from xdg_base import xdg_cache_path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CoverCache:
|
|
"""Manages a local cache of cover art images for the player UI.
|
|
|
|
The cache stores JPEG thumbnails keyed by (artist, album) hash.
|
|
Covers are stored in $XDG_CACHE_HOME/neo-pod-desktop/covers/
|
|
(default ~/.cache/neo-pod-desktop/covers/).
|
|
"""
|
|
|
|
def __init__(self, cache_dir: str | None = None):
|
|
self.cache_dir = cache_dir or xdg_cache_path("covers")
|
|
os.makedirs(self.cache_dir, exist_ok=True)
|
|
|
|
def _key(self, artist: str, album: str) -> str:
|
|
raw = f"{artist.lower()}|{album.lower()}"
|
|
return hashlib.md5(raw.encode()).hexdigest()
|
|
|
|
def get(self, artist: str, album: str) -> str | None:
|
|
"""Return path to cached cover, or None."""
|
|
path = os.path.join(self.cache_dir, f"{self._key(artist, album)}.jpg")
|
|
if os.path.exists(path):
|
|
return path
|
|
return None
|
|
|
|
def put(self, artist: str, album: str, art_bytes: bytes) -> str | None:
|
|
"""Cache cover art bytes and return the file path."""
|
|
from PIL import Image
|
|
from io import BytesIO
|
|
|
|
key = self._key(artist, album)
|
|
path = os.path.join(self.cache_dir, f"{key}.jpg")
|
|
try:
|
|
img = Image.open(BytesIO(art_bytes))
|
|
img = img.convert("RGB")
|
|
img.thumbnail((300, 300), Image.Resampling.LANCZOS)
|
|
img.save(path, "JPEG", quality=85)
|
|
return path
|
|
except Exception as exc:
|
|
logger.warning("Failed to cache cover art: %s", exc)
|
|
return None
|
|
|
|
def clear(self) -> None:
|
|
"""Remove all cached covers."""
|
|
for fname in os.listdir(self.cache_dir):
|
|
if fname.endswith(".jpg"):
|
|
try:
|
|
os.remove(os.path.join(self.cache_dir, fname))
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
cover_cache = CoverCache()
|