Maksim Totmin ba7144c88f feat: migrate LibraryCache to SQLite, add SessionStore + splash overlay
- 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
2026-06-01 22:12:56 +07:00

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()