""" cache.py — lightweight SQLite cache for scraped Tenor results. HF Spaces free tier gives you a persistent disk mounted at /data if you enable "persistent storage", otherwise the filesystem resets on restart. Either way this works fine — it just won't survive a restart without persistent storage enabled. Path is overridable via TENOR_CACHE_PATH env var. """ import os import json import sqlite3 import time import threading DB_PATH = os.environ.get("TENOR_CACHE_PATH", "/data/tenor_cache.db") # fall back to local dir if /data doesn't exist (no persistent storage enabled) if not os.path.isdir(os.path.dirname(DB_PATH)): DB_PATH = os.environ.get("TENOR_CACHE_PATH_FALLBACK", "./tenor_cache.db") _lock = threading.Lock() def _connect(): conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row return conn def init_db(): with _lock: conn = _connect() conn.execute(""" CREATE TABLE IF NOT EXISTS search_cache ( query TEXT NOT NULL, page INTEGER NOT NULL DEFAULT 1, results_json TEXT NOT NULL, cached_at REAL NOT NULL, PRIMARY KEY (query, page) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS favorites ( id INTEGER PRIMARY KEY AUTOINCREMENT, gif_url TEXT NOT NULL UNIQUE, mp4_url TEXT, title TEXT, tag TEXT, added_at REAL NOT NULL ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS request_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, endpoint TEXT NOT NULL, query TEXT, api_key TEXT, ts REAL NOT NULL ) """) conn.commit() conn.close() def get_cached_search(query, page=1, max_age_seconds=3600): with _lock: conn = _connect() row = conn.execute( "SELECT results_json, cached_at FROM search_cache WHERE query = ? AND page = ?", (query.lower().strip(), page), ).fetchone() conn.close() if not row: return None if time.time() - row["cached_at"] > max_age_seconds: return None return json.loads(row["results_json"]) def set_cached_search(query, results, page=1): with _lock: conn = _connect() conn.execute( "INSERT OR REPLACE INTO search_cache (query, page, results_json, cached_at) VALUES (?, ?, ?, ?)", (query.lower().strip(), page, json.dumps(results), time.time()), ) conn.commit() conn.close() def add_favorite(gif_url, mp4_url=None, title="", tag=""): with _lock: conn = _connect() try: conn.execute( "INSERT OR IGNORE INTO favorites (gif_url, mp4_url, title, tag, added_at) VALUES (?, ?, ?, ?, ?)", (gif_url, mp4_url, title, tag, time.time()), ) conn.commit() finally: conn.close() def remove_favorite(favorite_id): with _lock: conn = _connect() conn.execute("DELETE FROM favorites WHERE id = ?", (favorite_id,)) conn.commit() conn.close() def list_favorites(tag=None): with _lock: conn = _connect() if tag: rows = conn.execute( "SELECT * FROM favorites WHERE tag = ? ORDER BY added_at DESC", (tag,) ).fetchall() else: rows = conn.execute( "SELECT * FROM favorites ORDER BY added_at DESC" ).fetchall() conn.close() return [dict(r) for r in rows] def log_request(endpoint, query=None, api_key=None): with _lock: conn = _connect() conn.execute( "INSERT INTO request_log (endpoint, query, api_key, ts) VALUES (?, ?, ?, ?)", (endpoint, query, api_key, time.time()), ) conn.commit() conn.close() def clear_expired(max_age_seconds=86400): with _lock: conn = _connect() conn.execute( "DELETE FROM search_cache WHERE cached_at < ?", (time.time() - max_age_seconds,), ) conn.commit() conn.close()