Files
rippy/docker/api/cache.py
T
Hitonabi 95c1f9b105 feat(ui): SoC Refactoring & Config Validation
- Theme Context ausgelagert (ThemeContext.tsx + useDarkMode.ts)
- Config Validation mit TMDB-API-Key Pflicht (config_validation.py)
- Cache Key Centralization (cache/keys.py)
- CD-Ripping mit abcde implementiert (Worker)
- Docker Compose mit Healthchecks & LOG_LEVEL
- ROADMAP.md & SAVEPOINT.md aktualisiert
2026-07-22 16:38:19 +02:00

145 lines
3.7 KiB
Python

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