From 327275a3fa569cc2772a22cc935daa3419844838 Mon Sep 17 00:00:00 2001 From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:35:32 -0700 Subject: [PATCH] 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. --- api/wishlist.py | 34 ++++++++------------------- database/music_database.py | 48 +++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/api/wishlist.py b/api/wishlist.py index bff40e63..7482ed47 100644 --- a/api/wishlist.py +++ b/api/wishlist.py @@ -19,21 +19,19 @@ def register_routes(bp): fields = parse_fields(request) profile_id = parse_profile_id(request) + category_filter = category if category in ("singles", "albums") else None + try: from database.music_database import get_database db = get_database() - raw_tracks = db.get_wishlist_tracks(profile_id=profile_id) - - # Category filter - if category in ("singles", "albums"): - raw_tracks = [ - t for t in raw_tracks - if _track_category(t) == category - ] - - total = len(raw_tracks) - start = (page - 1) * limit - tracks = raw_tracks[start:start + limit] + offset = (page - 1) * limit + tracks = db.get_wishlist_tracks( + profile_id=profile_id, + category=category_filter, + limit=limit, + offset=offset, + ) + total = db.get_wishlist_count(profile_id=profile_id, category=category_filter) return api_success( {"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) except Exception as e: 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" diff --git a/database/music_database.py b/database/music_database.py index 0690d1ab..895367ec 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -6632,8 +6632,14 @@ class MusicDatabase: logger.error(f"Error removing track from wishlist: {e}") return False - def get_wishlist_tracks(self, limit: Optional[int] = None, profile_id: int = 1) -> List[Dict[str, Any]]: - """Get all tracks in the wishlist for the given profile, ordered by date added (oldest first for retry priority)""" + def get_wishlist_tracks(self, limit: Optional[int] = None, profile_id: int = 1, + 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: with self._get_connection() as conn: cursor = conn.cursor() @@ -6643,12 +6649,26 @@ class MusicDatabase: last_attempted, date_added, source_type, source_info FROM wishlist_tracks 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: - query += f" LIMIT {limit}" + query += " LIMIT ?" + params.append(int(limit)) + if offset: + query += " OFFSET ?" + params.append(int(offset)) cursor.execute(query, params) rows = cursor.fetchall() @@ -6679,7 +6699,7 @@ class MusicDatabase: except Exception as e: logger.error(f"Error getting wishlist tracks: {e}") return [] - + 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""" try: @@ -6706,12 +6726,22 @@ class MusicDatabase: logger.error(f"Error updating wishlist retry status: {e}") return False - def get_wishlist_count(self, profile_id: int = 1) -> int: - """Get the total number of tracks in the wishlist for the given profile""" + 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, + optionally filtered by category ('singles' or 'albums').""" try: with self._get_connection() as conn: 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() return result[0] if result else 0 except Exception as e: