518155051f
- 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
119 lines
2.9 KiB
Python
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()
|