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
|
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:
|
def _upgrade_spotify_image_url(url: str) -> str:
|
||||||
"""Upgrade a Spotify CDN image URL to the highest available resolution.
|
"""Upgrade a Spotify CDN image URL to the highest available resolution.
|
||||||
|
|
||||||
|
|
@ -744,21 +774,10 @@ class SpotifyClient:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Tokens live in the database-backed config store, not a loose
|
auth_manager = build_spotify_oauth_auth_manager(config_manager, config=config)
|
||||||
# file: config/.spotify_cache sat in /app/config, which is only
|
if auth_manager is None:
|
||||||
# persistent when the user's compose maps it — an anonymous
|
logger.warning("Spotify credentials not configured")
|
||||||
# volume is recreated empty on every container pull, so tokens
|
return
|
||||||
# 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sp = spotipy.Spotify(auth_manager=auth_manager, retries=0, requests_timeout=15)
|
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
|
# retries=0: prevent spotipy from sleeping for Retry-After duration on 429s
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ from pathlib import Path
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
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():
|
def test_normalization():
|
||||||
# Whitespace + quotes are stripped (paste garbage); the redirect_uri's
|
# 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(None) == {}
|
||||||
assert normalize_spotify_oauth_config(config) == expected
|
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) ──
|
# ── create_or_update_playlist: export a mirrored playlist back to Spotify (#945) ──
|
||||||
|
|
||||||
from core.spotify_client import SpotifyClient as _SpotifyClient
|
from core.spotify_client import SpotifyClient as _SpotifyClient
|
||||||
|
|
|
||||||
|
|
@ -5195,8 +5195,11 @@ def spotify_callback():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from core.spotify_client import SpotifyClient, normalize_spotify_oauth_config
|
from core.spotify_client import (
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
SpotifyClient,
|
||||||
|
build_spotify_oauth_auth_manager,
|
||||||
|
normalize_spotify_oauth_config,
|
||||||
|
)
|
||||||
from config.settings import config_manager
|
from config.settings import config_manager
|
||||||
|
|
||||||
# Per-profile callback: the profile's own account via the shared app.
|
# 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")
|
configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback")
|
||||||
logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
|
logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
|
||||||
|
|
||||||
auth_manager = SpotifyOAuth(
|
auth_manager = build_spotify_oauth_auth_manager(config_manager)
|
||||||
client_id=config['client_id'],
|
if not auth_manager:
|
||||||
client_secret=config['client_secret'],
|
raise Exception("Spotify credentials not configured")
|
||||||
redirect_uri=configured_uri,
|
|
||||||
scope=SPOTIFY_OAUTH_SCOPE,
|
|
||||||
cache_path='config/.spotify_cache'
|
|
||||||
)
|
|
||||||
|
|
||||||
token_info = auth_manager.get_access_token(auth_code)
|
token_info = auth_manager.get_access_token(auth_code)
|
||||||
|
|
||||||
|
|
@ -5244,6 +5243,7 @@ def spotify_callback():
|
||||||
global spotify_client
|
global spotify_client
|
||||||
clear_cached_metadata_client("spotify")
|
clear_cached_metadata_client("spotify")
|
||||||
spotify_client = get_spotify_client()
|
spotify_client = get_spotify_client()
|
||||||
|
spotify_client._invalidate_auth_cache()
|
||||||
if spotify_client.is_spotify_authenticated():
|
if spotify_client.is_spotify_authenticated():
|
||||||
# Clear any active rate limit ban and post-ban cooldown
|
# Clear any active rate limit ban and post-ban cooldown
|
||||||
# so Spotify is immediately usable after re-auth
|
# 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
|
# Manually trigger the token exchange using spotipy's auth manager
|
||||||
try:
|
try:
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
|
||||||
from config.settings import config_manager
|
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
|
# Get Spotify config
|
||||||
config = normalize_spotify_oauth_config(config_manager.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")
|
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}")
|
_oauth_logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
|
||||||
|
|
||||||
# Create auth manager and exchange code for token
|
auth_manager = build_spotify_oauth_auth_manager(config_manager)
|
||||||
auth_manager = SpotifyOAuth(
|
if not auth_manager:
|
||||||
client_id=config['client_id'],
|
raise Exception("Spotify credentials not configured")
|
||||||
client_secret=config['client_secret'],
|
|
||||||
redirect_uri=configured_uri,
|
|
||||||
scope=SPOTIFY_OAUTH_SCOPE,
|
|
||||||
cache_path='config/.spotify_cache'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract the authorization code and exchange it for tokens
|
|
||||||
token_info = auth_manager.get_access_token(auth_code)
|
token_info = auth_manager.get_access_token(auth_code)
|
||||||
|
|
||||||
if token_info:
|
if token_info:
|
||||||
|
|
@ -36128,6 +36124,7 @@ def start_oauth_callback_servers():
|
||||||
global spotify_client
|
global spotify_client
|
||||||
clear_cached_metadata_client("spotify")
|
clear_cached_metadata_client("spotify")
|
||||||
spotify_client = get_spotify_client()
|
spotify_client = get_spotify_client()
|
||||||
|
spotify_client._invalidate_auth_cache()
|
||||||
|
|
||||||
if spotify_client.is_spotify_authenticated():
|
if spotify_client.is_spotify_authenticated():
|
||||||
# Clear rate limit ban + post-ban cooldown so Spotify is usable immediately
|
# Clear rate limit ban + post-ban cooldown so Spotify is usable immediately
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue