"""SQLite Cache für API-Rate-Limits (LRU, 10k Einträge, TTL).""" import sqlite3 import time import json from pathlib import Path from typing import Any, Dict, Optional from cache.keys import PRESCAN_AUDIO_PREFIX, PRESCAN_VIDEO_PREFIX, CONFIRMED_METADATA_PREFIX CACHE_DIR = Path("/app/cache") CACHE_FILE = CACHE_DIR / "api_cache.db" MAX_ENTRIES = 10000 DEFAULT_TTL = 86400 # 24 Stunden def is_prescan_key(key: str) -> bool: """Prüfe ob Key ein Pre-Scan-Key ist.""" return key.startswith(PRESCAN_AUDIO_PREFIX) or key.startswith(PRESCAN_VIDEO_PREFIX) def is_confirmed_key(key: str) -> bool: """Prüfe ob Key ein bestätigter Metadaten-Key ist.""" return key.startswith(CONFIRMED_METADATA_PREFIX) def get_cache_category(key: str) -> str: """Ermittle Kategorie eines Cache-Keys.""" if is_prescan_key(key): return "prescan" elif is_confirmed_key(key): return "confirmed" elif key.startswith("tmdb:"): return "tmdb" elif key.startswith("musicbrainz:"): return "musicbrainz" return "other" def init_cache() -> None: """Initialisiere Cache-DB.""" CACHE_DIR.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(CACHE_FILE) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS api_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL, created_at INTEGER NOT NULL, ttl INTEGER NOT NULL ) """) cursor.execute("CREATE INDEX IF NOT EXISTS idx_key ON api_cache(key)") conn.commit() conn.close() def _get_connection() -> sqlite3.Connection: """Hole DB-Verbindung mit LRU-Check.""" conn = sqlite3.connect(CACHE_FILE) # LRU: entferne alte Einträge wenn MAX_ENTRIES überschritten cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM api_cache") count = cursor.fetchone()[0] if count >= MAX_ENTRIES: cursor.execute(""" DELETE FROM api_cache WHERE id IN ( SELECT id FROM api_cache ORDER BY created_at ASC LIMIT ? ) """, (MAX_ENTRIES // 10,)) conn.commit() return conn def get(key: str) -> Optional[Dict[str, Any]]: """Hole Wert aus Cache.""" conn = _get_connection() cursor = conn.cursor() cursor.execute( "SELECT value, created_at, ttl FROM api_cache WHERE key = ?", (key,) ) row = cursor.fetchone() if row is None: conn.close() return None value, created_at, ttl = row if time.time() > created_at + ttl: # TTL abgelaufen cursor.execute("DELETE FROM api_cache WHERE key = ?", (key,)) conn.commit() conn.close() return None conn.close() return json.loads(value) def set(key: str, value: Dict[str, Any], ttl: int = DEFAULT_TTL) -> None: """Speichere Wert im Cache.""" conn = _get_connection() cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO api_cache (key, value, created_at, ttl) VALUES (?, ?, ?, ?) """, (key, json.dumps(value), int(time.time()), ttl)) conn.commit() conn.close() def delete(key: str) -> None: """Lösche Eintrag aus Cache.""" conn = _get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM api_cache WHERE key = ?", (key,)) conn.commit() conn.close() def clear() -> None: """Lösche gesamten Cache.""" conn = _get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM api_cache") conn.commit() conn.close()