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,220 @@
|
||||
"""
|
||||
Recently Played Buffer using DragonflyDB.
|
||||
|
||||
Provides instant access to recently played tracks using a fast circular buffer
|
||||
stored in DragonflyDB. This eliminates the need for database queries for the
|
||||
most common "recently played" use case.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from swingmusic.db.dragonfly_client import get_dragonfly_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Maximum number of tracks to keep in the recently played buffer
|
||||
MAX_BUFFER_SIZE = 100
|
||||
|
||||
# TTL for recently played entries (30 days)
|
||||
BUFFER_TTL = 30 * 24 * 60 * 60
|
||||
|
||||
|
||||
class RecentlyPlayedBuffer:
|
||||
"""
|
||||
Manages recently played tracks using DragonflyDB lists.
|
||||
|
||||
Uses a circular buffer pattern with Redis lists (LPUSH + LTRIM)
|
||||
to maintain a fixed-size buffer of recently played tracks per user.
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = MAX_BUFFER_SIZE):
|
||||
self.max_size = max_size
|
||||
self._client = None
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
self._client = get_dragonfly_client()
|
||||
return self._client
|
||||
|
||||
def _get_key(self, userid: int) -> str:
|
||||
"""Get the Redis key for a user's recently played buffer."""
|
||||
return f"recently_played:user:{userid}"
|
||||
|
||||
def add_track(self, userid: int, track_data: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Add a track to the user's recently played buffer.
|
||||
|
||||
Args:
|
||||
userid: The user ID
|
||||
track_data: Track metadata including trackhash, title, artist, etc.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.client.is_available():
|
||||
return False
|
||||
|
||||
try:
|
||||
key = self._get_key(userid)
|
||||
|
||||
# Add timestamp to track data
|
||||
entry = {
|
||||
**track_data,
|
||||
"played_at": int(time.time()),
|
||||
}
|
||||
|
||||
# Use pipeline for atomic operations
|
||||
pipe = self.client.client.pipeline()
|
||||
|
||||
# Push to front of list
|
||||
pipe.lpush(key, json.dumps(entry))
|
||||
|
||||
# Trim to max size (keep only first max_size elements)
|
||||
pipe.ltrim(key, 0, self.max_size - 1)
|
||||
|
||||
# Set TTL
|
||||
pipe.expire(key, BUFFER_TTL)
|
||||
|
||||
pipe.execute()
|
||||
|
||||
logger.debug(f"Added track to recently played for user {userid}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add track to recently played buffer: {e}")
|
||||
return False
|
||||
|
||||
def get_recent_tracks(
|
||||
self, userid: int, limit: int = 20, offset: int = 0
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get recently played tracks for a user.
|
||||
|
||||
Args:
|
||||
userid: The user ID
|
||||
limit: Maximum number of tracks to return
|
||||
offset: Number of tracks to skip
|
||||
|
||||
Returns:
|
||||
List of track data dictionaries, most recent first
|
||||
"""
|
||||
if not self.client.is_available():
|
||||
return []
|
||||
|
||||
try:
|
||||
key = self._get_key(userid)
|
||||
|
||||
# Get range from list (LRANGE is 0-indexed, inclusive)
|
||||
end = offset + limit - 1
|
||||
results = self.client.client.lrange(key, offset, end)
|
||||
|
||||
tracks = []
|
||||
for result in results:
|
||||
try:
|
||||
tracks.append(json.loads(result))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return tracks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get recently played tracks: {e}")
|
||||
return []
|
||||
|
||||
def get_track_count(self, userid: int) -> int:
|
||||
"""Get the number of tracks in the user's recently played buffer."""
|
||||
if not self.client.is_available():
|
||||
return 0
|
||||
|
||||
try:
|
||||
key = self._get_key(userid)
|
||||
return self.client.client.llen(key)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def clear_buffer(self, userid: int) -> bool:
|
||||
"""Clear the recently played buffer for a user."""
|
||||
if not self.client.is_available():
|
||||
return False
|
||||
|
||||
try:
|
||||
key = self._get_key(userid)
|
||||
self.client.client.delete(key)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_track(self, userid: int, trackhash: str) -> bool:
|
||||
"""
|
||||
Remove a specific track from the buffer.
|
||||
|
||||
Note: This requires reading, filtering, and rewriting the list,
|
||||
so it's more expensive than other operations.
|
||||
"""
|
||||
if not self.client.is_available():
|
||||
return False
|
||||
|
||||
try:
|
||||
key = self._get_key(userid)
|
||||
|
||||
# Get all tracks
|
||||
all_tracks = self.client.client.lrange(key, 0, -1)
|
||||
|
||||
# Filter out the track to remove
|
||||
filtered = []
|
||||
for track_json in all_tracks:
|
||||
track = json.loads(track_json)
|
||||
if track.get("trackhash") != trackhash:
|
||||
filtered.append(track_json)
|
||||
|
||||
# Delete and rewrite if changed
|
||||
if len(filtered) != len(all_tracks):
|
||||
pipe = self.client.client.pipeline()
|
||||
pipe.delete(key)
|
||||
if filtered:
|
||||
pipe.rpush(key, *filtered)
|
||||
pipe.expire(key, BUFFER_TTL)
|
||||
pipe.execute()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove track from buffer: {e}")
|
||||
return False
|
||||
|
||||
def get_last_played_track(self, userid: int) -> dict[str, Any] | None:
|
||||
"""Get the most recently played track for a user."""
|
||||
tracks = self.get_recent_tracks(userid, limit=1)
|
||||
return tracks[0] if tracks else None
|
||||
|
||||
def is_track_recently_played(
|
||||
self, userid: int, trackhash: str, within_seconds: int = 3600
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a track was played recently (within the specified time).
|
||||
|
||||
Useful for preventing duplicate "recently played" entries.
|
||||
"""
|
||||
tracks = self.get_recent_tracks(userid, limit=10)
|
||||
now = int(time.time())
|
||||
|
||||
for track in tracks:
|
||||
if track.get("trackhash") == trackhash:
|
||||
played_at = track.get("played_at", 0)
|
||||
if now - played_at < within_seconds:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Global instance
|
||||
recently_played_buffer = RecentlyPlayedBuffer()
|
||||
|
||||
|
||||
def get_recently_played_buffer() -> RecentlyPlayedBuffer:
|
||||
"""Get the global recently played buffer instance."""
|
||||
return recently_played_buffer
|
||||
Reference in New Issue
Block a user