Source code for rekordbox_set_list_manager.services.collection_cache

"""On-disk cache for the Rekordbox collection to avoid slow re-loads.

The cache stores all tracks as a JSON byte-stream in the platformdirs
user-cache directory.  Validity is checked against the modification time of
the Rekordbox master.db file so the cache is automatically invalidated
whenever Rekordbox writes a new library snapshot.
"""

from __future__ import annotations

import contextlib
import json
import logging
from pathlib import Path

import platformdirs
from pydantic import TypeAdapter

from rekordbox_set_list_manager.models.track import Track

_CACHE_DIR = Path(platformdirs.user_cache_dir("rekordbox_set_list_manager"))
_CACHE_FILE = _CACHE_DIR / "collection.json"
_META_FILE = _CACHE_DIR / "collection_meta.json"

_ADAPTER: TypeAdapter[list[Track]] = TypeAdapter(list[Track])

[docs] log = logging.getLogger(__name__)
# --------------------------------------------------------------------------- # Public API # ---------------------------------------------------------------------------
[docs] def is_valid(db_path: Path) -> bool: """Return ``True`` if the cached collection is up to date with *db_path*. Parameters ---------- db_path : Path Path to the Rekordbox SQLite database whose mtime is checked. Returns ------- bool ``True`` if the cache exists and its recorded mtime matches *db_path*. """ if not _CACHE_FILE.exists() or not _META_FILE.exists(): return False try: meta: dict = json.loads(_META_FILE.read_text(encoding="utf-8")) cached_mtime: float = float(meta.get("db_mtime", 0.0)) current_mtime: float = db_path.stat().st_mtime return abs(current_mtime - cached_mtime) < 1.0 except Exception: # noqa: BLE001 return False
[docs] def load() -> list[Track] | None: """Load tracks from the cache. Returns ``None`` if missing or corrupt. Returns ------- list[Track] | None The cached tracks, or ``None`` if the cache file is absent or invalid. """ if not _CACHE_FILE.exists(): return None try: return _ADAPTER.validate_json(_CACHE_FILE.read_bytes()) except Exception as exc: # noqa: BLE001 log.warning("Failed to read collection cache: %s", exc) return None
[docs] def load_if_valid(db_path: Path) -> list[Track] | None: """Return cached tracks if the cache is valid for *db_path*, else ``None``. Parameters ---------- db_path : Path Path to the Rekordbox SQLite database to validate the cache against. Returns ------- list[Track] | None The cached track list if valid, or ``None`` if the cache is stale or absent. """ if not is_valid(db_path): return None return load()
[docs] def save(tracks: list[Track], db_path: Path) -> None: """Persist *tracks* and the current mtime of *db_path* to disk. Parameters ---------- tracks : list[Track] The collection tracks to serialise and cache. db_path : Path Path to the Rekordbox database; its mtime is stored for cache validation. """ _CACHE_DIR.mkdir(parents=True, exist_ok=True) try: _CACHE_FILE.write_bytes(_ADAPTER.dump_json(tracks)) mtime = db_path.stat().st_mtime _META_FILE.write_text( json.dumps({"db_path": str(db_path), "db_mtime": mtime}), encoding="utf-8", ) except Exception as exc: # noqa: BLE001 log.warning("Failed to save collection cache: %s", exc)
[docs] def invalidate() -> None: """Delete the cache files (called when the user explicitly forces a reload).""" for f in (_CACHE_FILE, _META_FILE): with contextlib.suppress(Exception): f.unlink(missing_ok=True)