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
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user