mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 12:33:03 +00:00
386 lines
11 KiB
Python
386 lines
11 KiB
Python
"""
|
|
Native DragonflyDB Client for SwingMusic
|
|
|
|
Integrated as a native database service like SQLite, providing:
|
|
- Ultra-fast caching for all services
|
|
- Session management
|
|
- User preferences
|
|
- Temporary data storage
|
|
- Real-time features
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DragonflyDBClient:
|
|
"""
|
|
Native DragonflyDB client integrated into SwingMusic
|
|
Provides Redis-compatible operations with automatic fallback
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
host: str | None = None,
|
|
port: int | None = None,
|
|
db: int | None = None,
|
|
):
|
|
self.host = host or os.environ.get("DRAGONFLYDB_HOST", "localhost")
|
|
self.port = port or int(os.environ.get("DRAGONFLYDB_PORT", "6379"))
|
|
self.db = db if db is not None else int(os.environ.get("DRAGONFLYDB_DB", "0"))
|
|
self.client = None
|
|
self.available = False
|
|
self._connect()
|
|
|
|
def _connect(self):
|
|
"""Connect to DragonflyDB with fallback handling"""
|
|
try:
|
|
import redis
|
|
|
|
self.client = redis.Redis(
|
|
host=self.host,
|
|
port=self.port,
|
|
db=self.db,
|
|
decode_responses=True,
|
|
socket_connect_timeout=2,
|
|
socket_timeout=2,
|
|
retry_on_timeout=True,
|
|
health_check_interval=30,
|
|
)
|
|
|
|
# Test connection
|
|
self.client.ping()
|
|
self.available = True
|
|
logger.info(f"✅ DragonflyDB connected at {self.host}:{self.port}")
|
|
|
|
except ImportError:
|
|
logger.warning("❌ Redis library not installed, DragonflyDB unavailable")
|
|
self.available = False
|
|
except Exception as e:
|
|
logger.warning(f"❌ DragonflyDB connection failed: {e}")
|
|
self.available = False
|
|
|
|
def is_available(self) -> bool:
|
|
"""Check if DragonflyDB is available"""
|
|
if not self.available or not self.client:
|
|
self._connect()
|
|
if not self.available or not self.client:
|
|
return False
|
|
|
|
try:
|
|
self.client.ping()
|
|
return True
|
|
except Exception:
|
|
self.available = False
|
|
return False
|
|
|
|
def set(self, key: str, value: Any, ttl: int | None = None) -> bool:
|
|
"""Set a key-value pair with optional TTL"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
serialized_value = (
|
|
json.dumps(value) if not isinstance(value, str) else value
|
|
)
|
|
|
|
if ttl:
|
|
return self.client.setex(key, ttl, serialized_value)
|
|
else:
|
|
return self.client.set(key, serialized_value)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB set failed: {e}")
|
|
return False
|
|
|
|
def get(self, key: str) -> Any | None:
|
|
"""Get a value by key"""
|
|
if not self.is_available():
|
|
return None
|
|
|
|
try:
|
|
value = self.client.get(key)
|
|
if value is None:
|
|
return None
|
|
|
|
# Try to deserialize as JSON
|
|
try:
|
|
return json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return value
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB get failed: {e}")
|
|
return None
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""Delete a key"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.client.delete(key))
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB delete failed: {e}")
|
|
return False
|
|
|
|
def exists(self, key: str) -> bool:
|
|
"""Check if key exists"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.client.exists(key))
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB exists failed: {e}")
|
|
return False
|
|
|
|
def expire(self, key: str, ttl: int) -> bool:
|
|
"""Set TTL for existing key"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.client.expire(key, ttl))
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB expire failed: {e}")
|
|
return False
|
|
|
|
def ttl(self, key: str) -> int:
|
|
"""Get TTL for key"""
|
|
if not self.is_available():
|
|
return -1
|
|
|
|
try:
|
|
return self.client.ttl(key)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB ttl failed: {e}")
|
|
return -1
|
|
|
|
def keys(self, pattern: str = "*") -> list[str]:
|
|
"""Get keys matching pattern"""
|
|
if not self.is_available():
|
|
return []
|
|
|
|
try:
|
|
return self.client.keys(pattern)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB keys failed: {e}")
|
|
return []
|
|
|
|
def incr(self, key: str, amount: int = 1) -> int:
|
|
"""Increment value by amount"""
|
|
if not self.is_available():
|
|
return 0
|
|
|
|
try:
|
|
return self.client.incr(key, amount)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB incr failed: {e}")
|
|
return 0
|
|
|
|
def lpush(self, key: str, *values) -> int:
|
|
"""Push values to left of list"""
|
|
if not self.is_available():
|
|
return 0
|
|
|
|
try:
|
|
return self.client.lpush(key, *values)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB lpush failed: {e}")
|
|
return 0
|
|
|
|
def rpop(self, key: str) -> str | None:
|
|
"""Pop value from right of list"""
|
|
if not self.is_available():
|
|
return None
|
|
|
|
try:
|
|
return self.client.rpop(key)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB rpop failed: {e}")
|
|
return None
|
|
|
|
def lrange(self, key: str, start: int, end: int) -> list[str]:
|
|
"""Get range of list elements"""
|
|
if not self.is_available():
|
|
return []
|
|
|
|
try:
|
|
return self.client.lrange(key, start, end)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB lrange failed: {e}")
|
|
return []
|
|
|
|
def llen(self, key: str) -> int:
|
|
"""Get length of list"""
|
|
if not self.is_available():
|
|
return 0
|
|
|
|
try:
|
|
return self.client.llen(key)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB llen failed: {e}")
|
|
return 0
|
|
|
|
def lrem(self, key: str, count: int, value: str) -> int:
|
|
"""Remove elements from list"""
|
|
if not self.is_available():
|
|
return 0
|
|
|
|
try:
|
|
return self.client.lrem(key, count, value)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB lrem failed: {e}")
|
|
return 0
|
|
|
|
def ltrim(self, key: str, start: int, end: int) -> bool:
|
|
"""Trim list to range"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return self.client.ltrim(key, start, end)
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB ltrim failed: {e}")
|
|
return False
|
|
|
|
def flushdb(self) -> bool:
|
|
"""Clear all keys in current database"""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return self.client.flushdb()
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB flushdb failed: {e}")
|
|
return False
|
|
|
|
def ping(self) -> bool:
|
|
"""Ping DragonflyDB."""
|
|
if not self.is_available():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.client.ping())
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB ping failed: {e}")
|
|
self.available = False
|
|
return False
|
|
|
|
def info(self) -> dict[str, Any]:
|
|
"""Get DragonflyDB server info"""
|
|
if not self.is_available():
|
|
return {}
|
|
|
|
try:
|
|
info = self.client.info()
|
|
return {
|
|
"version": info.get("redis_version", "unknown"),
|
|
"used_memory": info.get("used_memory", 0),
|
|
"used_memory_human": info.get("used_memory_human", "0B"),
|
|
"connected_clients": info.get("connected_clients", 0),
|
|
"total_commands_processed": info.get("total_commands_processed", 0),
|
|
"keyspace_hits": info.get("keyspace_hits", 0),
|
|
"keyspace_misses": info.get("keyspace_misses", 0),
|
|
"uptime_in_seconds": info.get("uptime_in_seconds", 0),
|
|
}
|
|
except Exception as e:
|
|
logger.debug(f"DragonflyDB info failed: {e}")
|
|
return {}
|
|
|
|
def close(self):
|
|
"""Close DragonflyDB connection"""
|
|
if self.client:
|
|
try:
|
|
self.client.close()
|
|
logger.info("DragonflyDB connection closed")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# Global DragonflyDB instance (like SQLite)
|
|
_dragonfly_client: DragonflyDBClient | None = None
|
|
|
|
|
|
def get_dragonfly_client() -> DragonflyDBClient:
|
|
"""Get the global DragonflyDB client instance"""
|
|
global _dragonfly_client
|
|
if _dragonfly_client is None:
|
|
_dragonfly_client = DragonflyDBClient()
|
|
return _dragonfly_client
|
|
|
|
|
|
def init_dragonfly_if_available() -> bool:
|
|
"""Initialize DragonflyDB if available"""
|
|
client = get_dragonfly_client()
|
|
return client.is_available()
|
|
|
|
|
|
class DragonflyCache:
|
|
"""High-level cache interface using DragonflyDB"""
|
|
|
|
def __init__(self, prefix: str = "swingmusic"):
|
|
self.client = get_dragonfly_client()
|
|
self.prefix = prefix
|
|
|
|
def _make_key(self, key: str) -> str:
|
|
"""Create namespaced key"""
|
|
return f"{self.prefix}:{key}"
|
|
|
|
def set(self, key: str, value: Any, ttl_hours: int = 12) -> bool:
|
|
"""Set cache value with TTL in hours"""
|
|
ttl_seconds = ttl_hours * 3600
|
|
return self.client.set(self._make_key(key), value, ttl_seconds)
|
|
|
|
def get(self, key: str) -> Any | None:
|
|
"""Get cache value"""
|
|
return self.client.get(self._make_key(key))
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""Delete cache value"""
|
|
return self.client.delete(self._make_key(key))
|
|
|
|
def exists(self, key: str) -> bool:
|
|
"""Check if cache value exists"""
|
|
return self.client.exists(self._make_key(key))
|
|
|
|
def clear_all(self) -> bool:
|
|
"""Clear all SwingMusic cache entries"""
|
|
if not self.client.is_available():
|
|
return False
|
|
|
|
keys = self.client.keys(f"{self.prefix}:*")
|
|
if keys:
|
|
return self.client.client.delete(*keys) > 0
|
|
return True
|
|
|
|
|
|
# Native cache instances for different purposes
|
|
spotify_cache = DragonflyCache("spotify")
|
|
session_cache = DragonflyCache("session")
|
|
user_cache = DragonflyCache("user")
|
|
temp_cache = DragonflyCache("temp")
|
|
|
|
|
|
def get_spotify_cache() -> DragonflyCache:
|
|
"""Get Spotify metadata cache"""
|
|
return spotify_cache
|
|
|
|
|
|
def get_session_cache() -> DragonflyCache:
|
|
"""Get user session cache"""
|
|
return session_cache
|
|
|
|
|
|
def get_user_cache() -> DragonflyCache:
|
|
"""Get user preferences cache"""
|
|
return user_cache
|
|
|
|
|
|
def get_temp_cache() -> DragonflyCache:
|
|
"""Get temporary data cache"""
|
|
return temp_cache
|