wait before retrying tidal requests
This commit is contained in:
@ -145,7 +145,27 @@ export abstract class OauthPlaylistAdder {
|
|||||||
redirect_uri?: string,
|
redirect_uri?: string,
|
||||||
client_secret?: string,
|
client_secret?: string,
|
||||||
customHeader?: HeadersInit
|
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();
|
const params = new URLSearchParams();
|
||||||
params.append('client_id', clientId);
|
params.append('client_id', clientId);
|
||||||
params.append('grant_type', 'refresh_token');
|
params.append('grant_type', 'refresh_token');
|
||||||
@ -157,15 +177,20 @@ export abstract class OauthPlaylistAdder {
|
|||||||
params.append('redirect_uri', redirect_uri);
|
params.append('redirect_uri', redirect_uri);
|
||||||
}
|
}
|
||||||
this.logger.debug('sending token req', params);
|
this.logger.debug('sending token req', params);
|
||||||
const resp: OauthResponse = await fetch(tokenUrl, {
|
const response = await fetch(tokenUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
headers: customHeader
|
headers: customHeader
|
||||||
}).then((r) => r.json());
|
});
|
||||||
|
|
||||||
|
const resp: OauthResponse = await response.json();
|
||||||
this.logger.verbose('received access token', resp);
|
this.logger.verbose('received access token', resp);
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
this.logger.error('token resp error', resp);
|
this.logger.error('token resp error', resp);
|
||||||
return null;
|
return {
|
||||||
|
resp: null,
|
||||||
|
headers: response.headers
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!resp.refresh_token) {
|
if (!resp.refresh_token) {
|
||||||
resp.refresh_token = refresh_token;
|
resp.refresh_token = refresh_token;
|
||||||
@ -175,6 +200,9 @@ export abstract class OauthPlaylistAdder {
|
|||||||
expiration.setSeconds(expiration.getSeconds() + resp.expires_in);
|
expiration.setSeconds(expiration.getSeconds() + resp.expires_in);
|
||||||
resp.expires = expiration;
|
resp.expires = expiration;
|
||||||
await fs.writeFile(this.token_file_name, JSON.stringify(resp));
|
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://openapi.tidal.com/v2', 'tidal_auth_token');
|
||||||
//super('https://api.tidal.com/v2', 'tidal_auth_token');
|
//super('https://api.tidal.com/v2', 'tidal_auth_token');
|
||||||
this.logger = new Logger('TidalPlaylistAdder');
|
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 {
|
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');
|
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) {
|
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 apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
|
||||||
const request = new Request(apiUrl, options);
|
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),
|
// 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
|
// 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 resp: Response | null = null;
|
||||||
|
let respTxt: string | null = null;
|
||||||
try {
|
try {
|
||||||
resp = await fetch(request);
|
resp = await fetch(request);
|
||||||
|
this.processTidalHeaders(resp.headers);
|
||||||
let respObj: TidalAddToPlaylistResponse | null = null;
|
let respObj: TidalAddToPlaylistResponse | null = null;
|
||||||
// If the request was successful, a 201 with no content is received
|
// If the request was successful, a 201 with no content is received
|
||||||
// Errors will have content and a different status code
|
// Errors will have content and a different status code
|
||||||
if (resp.status !== 201) {
|
if (resp.status !== 201 && resp.status !== 429) {
|
||||||
respObj = await resp.json();
|
respObj = await resp.json();
|
||||||
|
} else {
|
||||||
|
respTxt = await resp.text();
|
||||||
}
|
}
|
||||||
if (respObj !== null && respObj.errors) {
|
if (respObj !== null && respObj.errors) {
|
||||||
this.logger.error('Add to playlist failed', song.tidalUri, resp.status, 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--);
|
this.addToPlaylistRetry(song, remaning--);
|
||||||
}
|
}
|
||||||
} else if (respObj === null && resp.status === 201) {
|
} else if (respObj === null) {
|
||||||
|
switch (resp.status) {
|
||||||
|
case 201:
|
||||||
this.logger.info('Added to playlist', song.tidalUri, song.title);
|
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 {
|
} else {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'Add to playlist result is neither 201 nor error',
|
'Add to playlist result is neither 201 nor error',
|
||||||
|
Reference in New Issue
Block a user