Files
rippy/docker/api/cache.py
T
Hitonabi 518155051f Etappe 3: Metadaten-Lookup + Pre-Scan
- SQLite-Cache für API-Rate-Limits (LRU, 10k Einträge)
- TMDB/MusicBrainz/TheTVDB Clients
- Pre-Scan-Modul für TOC-Lesung ohne Ripping
- Metadaten-Preview UI
- Jellyfin-Formatierung (NFO + Images)
- API Endpoints für Lookup, Confirm, Format
2026-07-21 17:17:35 +02:00

119 lines
2.9 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
CACHE_DIR = Path("/app/cache")
CACHE_FILE = CACHE_DIR / "api_cache.db"
MAX_ENTRIES = 10000
DEFAULT_TTL = 86400 # 24 Stunden
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()