fix: use anonymous clienttoken as primary Spotify auth, Client Credentials as fallback
Developer Client ID is 24h rate-banned. clienttoken.spotify.com uses Spotify's own web-player client ID (separate rate-limit bucket). Also: fast-fail when Retry-After > 60s and invalidate the cached token so the next request immediately fetches a fresh one. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3633b4c5fa
commit
61271bff27
1 file changed
+66
-5
@@ -116,11 +116,66 @@ let _spTokenExpiry = 0;
|
||||
|
||||
async function fetchSpotifyToken() {
|
||||
if (_spTokenCache && Date.now() < _spTokenExpiry) return _spTokenCache;
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// Approach 1: anonymous web-player token via clienttoken.spotify.com
|
||||
// Uses Spotify's own web-player client ID — separate rate-limit bucket from developer apps.
|
||||
try {
|
||||
const ctRes = await _fetchTimeout('https://clienttoken.spotify.com/v1/clienttoken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
client_data: {
|
||||
client_version: '1.2.52.442.g276ab0bb',
|
||||
client_id: 'd8a5ed958d274c2e8ee717e6a4b0971d',
|
||||
js_sdk_data: {
|
||||
device_brand: 'unknown', device_model: 'unknown',
|
||||
os: 'windows', os_version: 'NT 10.0',
|
||||
device_id: randomUUID(), device_type: 'computer',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (ctRes.ok) {
|
||||
const ct = await ctRes.json();
|
||||
const clientToken = ct.granted_token?.token;
|
||||
if (clientToken) {
|
||||
const atRes = await _fetchTimeout(
|
||||
'https://open.spotify.com/get_access_token?reason=transport&productType=web_player',
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Cookie': `sp_t=${randomUUID()}`,
|
||||
'Client-Token': clientToken,
|
||||
'Referer': 'https://open.spotify.com/',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (atRes.ok) {
|
||||
const at = await atRes.json();
|
||||
if (at.accessToken) {
|
||||
_spTokenCache = at.accessToken;
|
||||
_spTokenExpiry = (at.accessTokenExpirationTimestampMs || (Date.now() + 55 * 60 * 1000)) - 30000;
|
||||
console.log('[spotify] token ok (anonymous)');
|
||||
return _spTokenCache;
|
||||
}
|
||||
}
|
||||
console.log('[spotify] get_access_token status:', atRes.status);
|
||||
}
|
||||
}
|
||||
console.log('[spotify] anonymous token failed, trying Client Credentials');
|
||||
} catch (e) {
|
||||
console.log('[spotify] anonymous token error:', e.message);
|
||||
}
|
||||
|
||||
// Approach 2: official Client Credentials (requires SPOTIFY_CLIENT_ID / SECRET in env)
|
||||
if (!CONFIG.spotifyClientId || !CONFIG.spotifyClientSecret) {
|
||||
throw new Error(
|
||||
'Spotify-Credentials fehlen. Bitte SPOTIFY_CLIENT_ID und ' +
|
||||
'Spotify-Token nicht abrufbar. Bitte SPOTIFY_CLIENT_ID und ' +
|
||||
'SPOTIFY_CLIENT_SECRET in der Umgebung setzen. ' +
|
||||
'Setup-Anleitung: https://developer.spotify.com/dashboard'
|
||||
'Setup: https://developer.spotify.com/dashboard'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,7 +199,7 @@ async function fetchSpotifyToken() {
|
||||
|
||||
_spTokenCache = d.access_token;
|
||||
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
||||
console.log('[spotify] token ok');
|
||||
console.log('[spotify] token ok (Client Credentials)');
|
||||
return _spTokenCache;
|
||||
}
|
||||
|
||||
@@ -154,8 +209,14 @@ async function _spApi(path, token) {
|
||||
const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (r.status === 429) {
|
||||
const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10);
|
||||
const wait = Math.min(retryAfter + 1, 15) * 1000; // cap at 15s per retry
|
||||
console.log(`[spotify] 429 rate-limited, waiting ${wait / 1000}s (Retry-After: ${retryAfter}s)`);
|
||||
// Long Retry-After = token is banned — invalidate cache and fail immediately
|
||||
if (retryAfter > 60) {
|
||||
_spTokenCache = null;
|
||||
_spTokenExpiry = 0;
|
||||
throw new Error(`Spotify-Token für ${Math.round(retryAfter / 3600)}h gesperrt (Rate Limit). Token-Cache geleert — nächster Versuch holt neuen Token.`);
|
||||
}
|
||||
const wait = (retryAfter + 1) * 1000;
|
||||
console.log(`[spotify] 429 waiting ${retryAfter + 1}s`);
|
||||
await new Promise(res => setTimeout(res, wait));
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user