Fix per-profile ListenBrainz playlist cache scoping and stale data recovery

This commit is contained in:
Broque Thomas 2026-03-14 21:11:59 -07:00
parent 8f0b9518bc
commit 264e696fe3
3 changed files with 56 additions and 2 deletions

View file

@ -399,6 +399,35 @@ class ListenBrainzManager:
conn.close()
return playlists
def get_playlist_type(self, playlist_mbid: str) -> str:
"""Get the playlist_type for a cached playlist, or None if not found"""
conn = self._get_db_connection()
cursor = conn.cursor()
cursor.execute(
"SELECT playlist_type FROM listenbrainz_playlists WHERE playlist_mbid = ? AND profile_id = ?",
(playlist_mbid, self.profile_id)
)
row = cursor.fetchone()
conn.close()
return row[0] if row else None
def delete_cached_playlist(self, playlist_mbid: str):
"""Delete a cached playlist and its tracks (CASCADE handles tracks via FK)"""
conn = self._get_db_connection()
cursor = conn.cursor()
# Delete tracks first (SQLite FK CASCADE requires PRAGMA foreign_keys=ON)
cursor.execute("""
DELETE FROM listenbrainz_tracks WHERE playlist_id IN (
SELECT id FROM listenbrainz_playlists WHERE playlist_mbid = ? AND profile_id = ?
)
""", (playlist_mbid, self.profile_id))
cursor.execute(
"DELETE FROM listenbrainz_playlists WHERE playlist_mbid = ? AND profile_id = ?",
(playlist_mbid, self.profile_id)
)
conn.commit()
conn.close()
def get_cached_tracks(self, playlist_mbid: str) -> List[Dict]:
"""Get cached tracks for a playlist from database"""
conn = self._get_db_connection()

View file

@ -2328,6 +2328,17 @@ class MusicDatabase:
cursor.execute("DROP TABLE listenbrainz_playlists")
cursor.execute("ALTER TABLE listenbrainz_playlists_new RENAME TO listenbrainz_playlists")
# Clean up playlists that lost their tracks during table recreation
# (track playlist_id foreign keys may reference stale IDs).
# This forces a fresh re-fetch from ListenBrainz on next page load.
cursor.execute("""
DELETE FROM listenbrainz_playlists
WHERE id NOT IN (SELECT DISTINCT playlist_id FROM listenbrainz_tracks)
""")
cleaned = cursor.rowcount
if cleaned:
logger.info(f"Cleaned up {cleaned} stale playlists (will re-fetch from ListenBrainz)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_lb_playlists_profile ON listenbrainz_playlists (profile_id)")
cursor.execute("""

View file

@ -32009,7 +32009,8 @@ def get_discover_genre_playlist(genre_name):
# ===============================
def _get_profile_lb_manager():
"""Create a profile-aware ListenBrainzManager for the current user"""
"""Create a profile-aware ListenBrainzManager for the current user.
Always uses the actual profile_id so each profile has its own playlist cache."""
from core.listenbrainz_manager import ListenBrainzManager
profile_id = get_current_profile_id()
token, base_url, username, source = _get_lb_credentials_for_profile(profile_id)
@ -32089,11 +32090,24 @@ def get_listenbrainz_collaborative():
@app.route('/api/discover/listenbrainz/playlist/<playlist_mbid>', methods=['GET'])
def get_listenbrainz_playlist_tracks(playlist_mbid):
"""Get tracks from a specific ListenBrainz playlist (from cache)"""
"""Get tracks from a specific ListenBrainz playlist (from cache, with on-demand refresh)"""
try:
lb_manager, username, source = _get_profile_lb_manager()
tracks = lb_manager.get_cached_tracks(playlist_mbid)
if not tracks:
# Cache miss or stale entry with no tracks — try fetching from LB API
if lb_manager.client.is_authenticated():
print(f"🔄 Cache miss for playlist {playlist_mbid}, fetching from ListenBrainz...")
# Remove stale playlist row (if any) so _update_playlist doesn't
# skip due to matching track_count with 0 actual tracks
existing_type = lb_manager.get_playlist_type(playlist_mbid) or 'created_for'
lb_manager.delete_cached_playlist(playlist_mbid)
full_playlist = lb_manager.client.get_playlist_details(playlist_mbid)
if full_playlist:
lb_manager._update_playlist(full_playlist, existing_type)
tracks = lb_manager.get_cached_tracks(playlist_mbid)
if not tracks:
return jsonify({
"success": False,