wait before retrying tidal requests
This commit is contained in:
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
Reference in New Issue
Block a user