wait before retrying tidal requests

This commit is contained in:
2025-07-11 14:37:57 +02:00
parent 44fc2bb621
commit df35c48e8c
2 changed files with 98 additions and 10 deletions

View File

@ -145,7 +145,27 @@ export abstract class OauthPlaylistAdder {
redirect_uri?: string,
client_secret?: string,
customHeader?: HeadersInit
) {
): Promise<OauthResponse | null> {
return (
await this.requestRefreshTokenWithHeaders(
tokenUrl,
clientId,
refresh_token,
redirect_uri,
client_secret,
customHeader
)
).resp;
}
protected async requestRefreshTokenWithHeaders(
tokenUrl: URL,
clientId: string,
refresh_token: string,
redirect_uri?: string,
client_secret?: string,
customHeader?: HeadersInit
): Promise<{ resp: OauthResponse | null; headers: Headers }> {
const params = new URLSearchParams();
params.append('client_id', clientId);
params.append('grant_type', 'refresh_token');
@ -157,15 +177,20 @@ export abstract class OauthPlaylistAdder {
params.append('redirect_uri', redirect_uri);
}
this.logger.debug('sending token req', params);
const resp: OauthResponse = await fetch(tokenUrl, {
const response = await fetch(tokenUrl, {
method: 'POST',
body: params,
headers: customHeader
}).then((r) => r.json());
});
const resp: OauthResponse = await response.json();
this.logger.verbose('received access token', resp);
if (resp.error) {
this.logger.error('token resp error', resp);
return null;
return {
resp: null,
headers: response.headers
};
}
if (!resp.refresh_token) {
resp.refresh_token = refresh_token;
@ -175,6 +200,9 @@ export abstract class OauthPlaylistAdder {
expiration.setSeconds(expiration.getSeconds() + resp.expires_in);
resp.expires = expiration;
await fs.writeFile(this.token_file_name, JSON.stringify(resp));
return resp;
return {
resp: resp,
headers: response.headers
};
}
}

View File

@ -14,6 +14,8 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
super('https://openapi.tidal.com/v2', 'tidal_auth_token');
//super('https://api.tidal.com/v2', 'tidal_auth_token');
this.logger = new Logger('TidalPlaylistAdder');
// Tidal aggressively rate-limits, so reduce the number of refreshing requests
this.refresh_time = 3;
}
public constructAuthUrl(redirectUri: URL): URL {
@ -67,7 +69,32 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
}
const tokenUrl = new URL('https://auth.tidal.com/v1/oauth2/token');
return await this.requestRefreshToken(tokenUrl, TIDAL_CLIENT_ID, token.refresh_token);
const response = await this.requestRefreshTokenWithHeaders(
tokenUrl,
TIDAL_CLIENT_ID,
token.refresh_token
);
this.processTidalHeaders(response.headers);
return response.resp;
}
private processTidalHeaders(headers: Headers) {
const remainingTokens = headers.get('x-ratelimit-remaining');
const requiredTokens = headers.get('x-ratelimit-requested-tokens');
const replenishRate = headers.get('x-ratelimit-replenish-rate');
if (remainingTokens !== null && replenishRate !== null) {
const remainingTokensValue = parseInt(remainingTokens);
const replenishRateValue = parseInt(replenishRate);
let requiredTokensValue = parseInt(requiredTokens ?? '-1');
this.logger.debug(
'Tidal rate limit. Remaining',
remainingTokensValue,
'reuqired for last request',
requiredTokensValue,
'replenish rate',
replenishRateValue
);
}
}
private async addToPlaylistRetry(song: SongInfo, remaning: number = 3) {
@ -112,7 +139,6 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
};
const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
const request = new Request(apiUrl, options);
this.logger.debug('Adding to playlist request', request);
// This would be API v1 (or api v2, but *not* the OpenAPI v2),
// but that requires r_usr and w_usr permission scopes which are impossible to request
@ -150,13 +176,17 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
*/
let resp: Response | null = null;
let respTxt: string | null = null;
try {
resp = await fetch(request);
this.processTidalHeaders(resp.headers);
let respObj: TidalAddToPlaylistResponse | null = null;
// If the request was successful, a 201 with no content is received
// Errors will have content and a different status code
if (resp.status !== 201) {
if (resp.status !== 201 && resp.status !== 429) {
respObj = await resp.json();
} else {
respTxt = await resp.text();
}
if (respObj !== null && respObj.errors) {
this.logger.error('Add to playlist failed', song.tidalUri, resp.status, respObj.errors);
@ -167,8 +197,38 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
}
this.addToPlaylistRetry(song, remaning--);
}
} else if (respObj === null && resp.status === 201) {
this.logger.info('Added to playlist', song.tidalUri, song.title);
} else if (respObj === null) {
switch (resp.status) {
case 201:
this.logger.info('Added to playlist', song.tidalUri, song.title);
break;
case 429:
const remainingTokens = resp.headers.get('x-ratelimit-remaining');
const requiredTokens = resp.headers.get('x-ratelimit-requested-tokens');
const replenishRate = resp.headers.get('x-ratelimit-replenish-rate');
if (remainingTokens !== null && requiredTokens !== null && replenishRate !== null) {
const remainingTokensValue = parseInt(remainingTokens);
const requiredTokensValue = parseInt(requiredTokens);
const replenishRateValue = parseInt(replenishRate);
const needToReplenish = requiredTokensValue - remainingTokensValue;
const secondsToWait = 1 + needToReplenish / replenishRateValue;
this.logger.warn(
'Received HTTP 429 Too Many Requests. Retrying in',
secondsToWait,
'sec'
);
// Try again secondsToWait sec later, just to be safe one additional second
setTimeout(() => {
this.addToPlaylistRetry(song, remaning--);
}, secondsToWait * 1000);
} else {
this.logger.warn('Could not read headers how long to wait', resp.headers);
}
break;
default:
this.logger.warn('Unknown response', resp.status, respTxt);
break;
}
} else {
this.logger.info(
'Add to playlist result is neither 201 nor error',