mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 20:43:04 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user