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() {
|
async function fetchSpotifyToken() {
|
||||||
if (_spTokenCache && Date.now() < _spTokenExpiry) return _spTokenCache;
|
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) {
|
if (!CONFIG.spotifyClientId || !CONFIG.spotifyClientSecret) {
|
||||||
throw new Error(
|
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. ' +
|
'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;
|
_spTokenCache = d.access_token;
|
||||||
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
||||||
console.log('[spotify] token ok');
|
console.log('[spotify] token ok (Client Credentials)');
|
||||||
return _spTokenCache;
|
return _spTokenCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,8 +209,14 @@ async function _spApi(path, token) {
|
|||||||
const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } });
|
const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } });
|
||||||
if (r.status === 429) {
|
if (r.status === 429) {
|
||||||
const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10);
|
const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10);
|
||||||
const wait = Math.min(retryAfter + 1, 15) * 1000; // cap at 15s per retry
|
// Long Retry-After = token is banned — invalidate cache and fail immediately
|
||||||
console.log(`[spotify] 429 rate-limited, waiting ${wait / 1000}s (Retry-After: ${retryAfter}s)`);
|
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));
|
await new Promise(res => setTimeout(res, wait));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user