fix spotify cache mismatch
This commit is contained in:
parent
5d5aac9be3
commit
88b2ed7ce9
3 changed files with 78 additions and 37 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue