fix spotify cache mismatch

This commit is contained in:
Siddharth Pradhan 2026-06-29 12:52:21 -04:00
parent 5d5aac9be3
commit 88b2ed7ce9
3 changed files with 78 additions and 37 deletions

View file

@ -53,6 +53,36 @@ def normalize_spotify_oauth_config(config: Optional[Dict[str, Any]]) -> Dict[str
return normalized
def build_spotify_oauth_auth_manager(
config_manager=None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Optional[SpotifyOAuth]:
"""Build SpotifyOAuth with the database-backed token cache.
OAuth callbacks and SpotifyClient must share the same cache store.
Callbacks that wrote to ``config/.spotify_cache`` while the client read
``spotify.token_info`` from the DB left validation probing with a stale or
missing token; spotipy then tried interactive OAuth on port 8888
EADDRINUSE inside Docker.
"""
from config.settings import config_manager as default_cm
from core.spotify_token_cache import DatabaseTokenCache
cm = config_manager or default_cm
cfg = normalize_spotify_oauth_config(config or cm.get_spotify_config())
if not cfg.get("client_id") or not cfg.get("client_secret"):
return None
return SpotifyOAuth(
client_id=cfg["client_id"],
client_secret=cfg["client_secret"],
redirect_uri=cfg.get("redirect_uri", "http://127.0.0.1:8888/callback"),
scope=SPOTIFY_OAUTH_SCOPE,
cache_handler=DatabaseTokenCache(cm),
)
def _upgrade_spotify_image_url(url: str) -> str:
"""Upgrade a Spotify CDN image URL to the highest available resolution.
@ -744,22 +774,11 @@ class SpotifyClient:
return
try:
# Tokens live in the database-backed config store, not a loose
# file: config/.spotify_cache sat in /app/config, which is only
# persistent when the user's compose maps it — an anonymous
# volume is recreated empty on every container pull, so tokens
# died nightly while every other setting survived ("it keeps
# unauthenticating" daily, wolf39us). The handler imports the
# legacy file once if the store is empty.
from core.spotify_token_cache import DatabaseTokenCache
auth_manager = SpotifyOAuth(
client_id=config['client_id'],
client_secret=config['client_secret'],
redirect_uri=config.get('redirect_uri', "http://127.0.0.1:8888/callback"),
scope=SPOTIFY_OAUTH_SCOPE,
cache_handler=DatabaseTokenCache(config_manager)
)
auth_manager = build_spotify_oauth_auth_manager(config_manager, config=config)
if auth_manager is None:
logger.warning("Spotify credentials not configured")
return
self.sp = spotipy.Spotify(auth_manager=auth_manager, retries=0, requests_timeout=15)
# retries=0: prevent spotipy from sleeping for Retry-After duration on 429s
# (can be hours). Our rate_limited decorator + global ban handle retries instead.

View file

@ -3,7 +3,10 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from core.spotify_client import normalize_spotify_oauth_config
from unittest.mock import MagicMock, patch
from core.spotify_client import build_spotify_oauth_auth_manager, normalize_spotify_oauth_config
from core.spotify_token_cache import DatabaseTokenCache
def test_normalization():
# Whitespace + quotes are stripped (paste garbage); the redirect_uri's
@ -89,6 +92,28 @@ def test_no_input():
assert normalize_spotify_oauth_config(None) == {}
assert normalize_spotify_oauth_config(config) == expected
def test_build_spotify_oauth_auth_manager_uses_database_cache():
cm = MagicMock()
cm.get_spotify_config.return_value = {
"client_id": "client_id",
"client_secret": "client_secret",
"redirect_uri": "http://127.0.0.1:8888/callback",
}
with patch("core.spotify_client.SpotifyOAuth") as mock_oauth:
auth_manager = build_spotify_oauth_auth_manager(cm)
assert auth_manager is mock_oauth.return_value
kwargs = mock_oauth.call_args.kwargs
assert isinstance(kwargs["cache_handler"], DatabaseTokenCache)
assert kwargs["scope"]
assert "cache_path" not in kwargs
def test_build_spotify_oauth_auth_manager_returns_none_without_creds():
cm = MagicMock()
cm.get_spotify_config.return_value = {"client_id": "", "client_secret": ""}
assert build_spotify_oauth_auth_manager(cm) is None
# ── create_or_update_playlist: export a mirrored playlist back to Spotify (#945) ──
from core.spotify_client import SpotifyClient as _SpotifyClient

View file

@ -5195,8 +5195,11 @@ def spotify_callback():
pass
try:
from core.spotify_client import SpotifyClient, normalize_spotify_oauth_config
from spotipy.oauth2 import SpotifyOAuth
from core.spotify_client import (
SpotifyClient,
build_spotify_oauth_auth_manager,
normalize_spotify_oauth_config,
)
from config.settings import config_manager
# Per-profile callback: the profile's own account via the shared app.
@ -5229,13 +5232,9 @@ def spotify_callback():
configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback")
logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
auth_manager = SpotifyOAuth(
client_id=config['client_id'],
client_secret=config['client_secret'],
redirect_uri=configured_uri,
scope=SPOTIFY_OAUTH_SCOPE,
cache_path='config/.spotify_cache'
)
auth_manager = build_spotify_oauth_auth_manager(config_manager)
if not auth_manager:
raise Exception("Spotify credentials not configured")
token_info = auth_manager.get_access_token(auth_code)
@ -5244,6 +5243,7 @@ def spotify_callback():
global spotify_client
clear_cached_metadata_client("spotify")
spotify_client = get_spotify_client()
spotify_client._invalidate_auth_cache()
if spotify_client.is_spotify_authenticated():
# Clear any active rate limit ban and post-ban cooldown
# so Spotify is immediately usable after re-auth
@ -36102,25 +36102,21 @@ def start_oauth_callback_servers():
# Manually trigger the token exchange using spotipy's auth manager
try:
from spotipy.oauth2 import SpotifyOAuth
from config.settings import config_manager
from core.spotify_client import normalize_spotify_oauth_config
from core.spotify_client import (
build_spotify_oauth_auth_manager,
normalize_spotify_oauth_config,
)
# Get Spotify config
config = normalize_spotify_oauth_config(config_manager.get_spotify_config())
configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback")
_oauth_logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
# Create auth manager and exchange code for token
auth_manager = SpotifyOAuth(
client_id=config['client_id'],
client_secret=config['client_secret'],
redirect_uri=configured_uri,
scope=SPOTIFY_OAUTH_SCOPE,
cache_path='config/.spotify_cache'
)
auth_manager = build_spotify_oauth_auth_manager(config_manager)
if not auth_manager:
raise Exception("Spotify credentials not configured")
# Extract the authorization code and exchange it for tokens
token_info = auth_manager.get_access_token(auth_code)
if token_info:
@ -36128,6 +36124,7 @@ def start_oauth_callback_servers():
global spotify_client
clear_cached_metadata_client("spotify")
spotify_client = get_spotify_client()
spotify_client._invalidate_auth_cache()
if spotify_client.is_spotify_authenticated():
# Clear rate limit ban + post-ban cooldown so Spotify is usable immediately