fix: push wishlist pagination to SQL

The wishlist list endpoint previously loaded and JSON-decoded the full
wishlist, filtered by category in Python, then sliced in memory. Cost
grew linearly with wishlist size on every page request.

get_wishlist_tracks now accepts offset and category parameters, both
applied in SQL via LIMIT/OFFSET and json_extract. get_wishlist_count
also accepts category so COUNT(*) matches the filtered page. The API
endpoint uses these to return only the requested page.

Backward compatible: other callers (core/wishlist_service) pass no
offset/category and still receive the full list.
This commit is contained in:
JohnBaumb 2026-04-19 14:35:32 -07:00
parent 133af45199
commit 327275a3fa
2 changed files with 49 additions and 33 deletions

View file

@ -19,21 +19,19 @@ def register_routes(bp):
fields = parse_fields(request) fields = parse_fields(request)
profile_id = parse_profile_id(request) profile_id = parse_profile_id(request)
category_filter = category if category in ("singles", "albums") else None
try: try:
from database.music_database import get_database from database.music_database import get_database
db = get_database() db = get_database()
raw_tracks = db.get_wishlist_tracks(profile_id=profile_id) offset = (page - 1) * limit
tracks = db.get_wishlist_tracks(
# Category filter profile_id=profile_id,
if category in ("singles", "albums"): category=category_filter,
raw_tracks = [ limit=limit,
t for t in raw_tracks offset=offset,
if _track_category(t) == category )
] total = db.get_wishlist_count(profile_id=profile_id, category=category_filter)
total = len(raw_tracks)
start = (page - 1) * limit
tracks = raw_tracks[start:start + limit]
return api_success( return api_success(
{"tracks": [serialize_wishlist_track(t, fields) for t in tracks]}, {"tracks": [serialize_wishlist_track(t, fields) for t in tracks]},
@ -104,15 +102,3 @@ def register_routes(bp):
return api_error("NOT_AVAILABLE", "Wishlist processing function not available.", 501) return api_error("NOT_AVAILABLE", "Wishlist processing function not available.", 501)
except Exception as e: except Exception as e:
return api_error("WISHLIST_ERROR", str(e), 500) return api_error("WISHLIST_ERROR", str(e), 500)
def _track_category(track):
"""Determine if a wishlist track is a single or album track."""
album_type = ""
if isinstance(track, dict):
sd = track.get("spotify_data", {})
if isinstance(sd, dict):
album = sd.get("album", {})
if isinstance(album, dict):
album_type = album.get("album_type", "")
return "albums" if album_type == "album" else "singles"

View file

@ -6632,8 +6632,14 @@ class MusicDatabase:
logger.error(f"Error removing track from wishlist: {e}") logger.error(f"Error removing track from wishlist: {e}")
return False return False
def get_wishlist_tracks(self, limit: Optional[int] = None, profile_id: int = 1) -> List[Dict[str, Any]]: def get_wishlist_tracks(self, limit: Optional[int] = None, profile_id: int = 1,
"""Get all tracks in the wishlist for the given profile, ordered by date added (oldest first for retry priority)""" offset: int = 0, category: Optional[str] = None) -> List[Dict[str, Any]]:
"""Get tracks in the wishlist for the given profile, ordered by date added
(oldest first for retry priority).
Supports SQL-level pagination via limit/offset and optional category
filtering (singles vs albums) pushed down to SQL using json_extract.
"""
try: try:
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@ -6643,12 +6649,26 @@ class MusicDatabase:
last_attempted, date_added, source_type, source_info last_attempted, date_added, source_type, source_info
FROM wishlist_tracks FROM wishlist_tracks
WHERE profile_id = ? WHERE profile_id = ?
ORDER BY date_added
""" """
params = [profile_id] params: List[Any] = [profile_id]
if category == "albums":
query += " AND json_extract(spotify_data, '$.album.album_type') = 'album'"
elif category == "singles":
query += (
" AND (json_extract(spotify_data, '$.album.album_type') IS NULL"
" OR json_extract(spotify_data, '$.album.album_type') != 'album')"
)
query += " ORDER BY date_added"
if limit: if limit:
query += f" LIMIT {limit}" query += " LIMIT ?"
params.append(int(limit))
if offset:
query += " OFFSET ?"
params.append(int(offset))
cursor.execute(query, params) cursor.execute(query, params)
rows = cursor.fetchall() rows = cursor.fetchall()
@ -6679,7 +6699,7 @@ class MusicDatabase:
except Exception as e: except Exception as e:
logger.error(f"Error getting wishlist tracks: {e}") logger.error(f"Error getting wishlist tracks: {e}")
return [] return []
def update_wishlist_retry(self, spotify_track_id: str, success: bool, error_message: str = None, profile_id: int = 1) -> bool: def update_wishlist_retry(self, spotify_track_id: str, success: bool, error_message: str = None, profile_id: int = 1) -> bool:
"""Update retry count and status for a wishlist track""" """Update retry count and status for a wishlist track"""
try: try:
@ -6706,12 +6726,22 @@ class MusicDatabase:
logger.error(f"Error updating wishlist retry status: {e}") logger.error(f"Error updating wishlist retry status: {e}")
return False return False
def get_wishlist_count(self, profile_id: int = 1) -> int: def get_wishlist_count(self, profile_id: int = 1, category: Optional[str] = None) -> int:
"""Get the total number of tracks in the wishlist for the given profile""" """Get the total number of tracks in the wishlist for the given profile,
optionally filtered by category ('singles' or 'albums')."""
try: try:
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM wishlist_tracks WHERE profile_id = ?", (profile_id,)) query = "SELECT COUNT(*) FROM wishlist_tracks WHERE profile_id = ?"
params: List[Any] = [profile_id]
if category == "albums":
query += " AND json_extract(spotify_data, '$.album.album_type') = 'album'"
elif category == "singles":
query += (
" AND (json_extract(spotify_data, '$.album.album_type') IS NULL"
" OR json_extract(spotify_data, '$.album.album_type') != 'album')"
)
cursor.execute(query, params)
result = cursor.fetchone() result = cursor.fetchone()
return result[0] if result else 0 return result[0] if result else 0
except Exception as e: except Exception as e: