mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 04:23:02 +00:00
221 lines
6.4 KiB
Python
221 lines
6.4 KiB
Python
"""
|
|
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
|