tidal: chunk playlist adding, fix next-paging handling
This commit is contained in:
@ -121,8 +121,13 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
|
||||
try {
|
||||
let albumUrl: URL;
|
||||
if (nextLink) {
|
||||
this.logger.debug('getAlbumItems nextPage', nextLink);
|
||||
albumUrl = new URL(nextLink);
|
||||
} else {
|
||||
this.logger.debug(
|
||||
'getAlbumItems albumUrl',
|
||||
`${this.apiBase}/albums/${albumId}/relationships/items`
|
||||
);
|
||||
albumUrl = new URL(`${this.apiBase}/albums/${albumId}/relationships/items`);
|
||||
albumUrl.searchParams.append('countryCode', 'DE');
|
||||
}
|
||||
@ -144,7 +149,12 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
|
||||
return { id: x.id, type: x.type };
|
||||
});
|
||||
if (respData.links?.next) {
|
||||
const nextPage = await this.getAlbumItems(albumId, remaning, respData.links.next);
|
||||
this.logger.debug('getAlbumItems requesting next page', respData.links.next);
|
||||
const nextPage = await this.getAlbumItems(
|
||||
albumId,
|
||||
remaning,
|
||||
this.apiBase + respData.links.next
|
||||
);
|
||||
tracks = tracks.concat(nextPage);
|
||||
}
|
||||
return tracks;
|
||||
@ -243,96 +253,108 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
|
||||
this.logger.error('Could not check for tidal dupes', dbe);
|
||||
}
|
||||
|
||||
// This would be API v2, but that's still in beta and only allows adding an item *before* another one
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${token.token_type} ${token.access_token}`,
|
||||
'Content-Type': 'application/vnd.api+json',
|
||||
Accept: 'application/vnd.api+json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: tracks,
|
||||
meta: {
|
||||
positionBefore: 'ffb6286e-237a-4dfc-bbf1-2fb0eb004ed5' // Hardcoded last element of list
|
||||
}
|
||||
})
|
||||
};
|
||||
const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
|
||||
const request = new Request(apiUrl, options);
|
||||
// Tidal can only handle max. 20 items to be added at once
|
||||
// This isn't documented, but the API helpfully provides a useful error message
|
||||
let chunkSize = 20;
|
||||
const chunkedTracks: { id: string; type: string }[][] = [];
|
||||
let chunkIndex = 0;
|
||||
while (chunkIndex < tracks.length) {
|
||||
chunkedTracks.push(tracks.slice(chunkIndex, chunkIndex + chunkSize));
|
||||
chunkIndex += chunkSize;
|
||||
}
|
||||
|
||||
const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
|
||||
let options: RequestInit;
|
||||
let request: Request;
|
||||
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 && 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);
|
||||
if (resp.status === 401 || respObj.errors.some((x) => x.code === 'UNAUTHORIZED')) {
|
||||
const token = await this.refreshToken(true);
|
||||
if (token == null) {
|
||||
return;
|
||||
for (let chunk of chunkedTracks) {
|
||||
options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `${token.token_type} ${token.access_token}`,
|
||||
'Content-Type': 'application/vnd.api+json',
|
||||
Accept: 'application/vnd.api+json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: chunk,
|
||||
meta: {
|
||||
positionBefore: 'ffb6286e-237a-4dfc-bbf1-2fb0eb004ed5' // Hardcoded last element of list
|
||||
}
|
||||
this.addToPlaylistRetry(song, remaning--);
|
||||
})
|
||||
};
|
||||
|
||||
request = new Request(apiUrl, options);
|
||||
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 && resp.status !== 429) {
|
||||
respObj = await resp.json();
|
||||
} else {
|
||||
respTxt = await resp.text();
|
||||
}
|
||||
} else if (respObj === null) {
|
||||
switch (resp.status) {
|
||||
case 201:
|
||||
this.logger.info('Added to playlist', song.tidalUri, song.title);
|
||||
break;
|
||||
case 429:
|
||||
let secondsToWait = -1;
|
||||
const retryAfterHeader = resp.headers.get('Retry-After');
|
||||
if (retryAfterHeader) {
|
||||
secondsToWait = parseInt(retryAfterHeader);
|
||||
} else {
|
||||
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;
|
||||
secondsToWait = 1 + needToReplenish / replenishRateValue;
|
||||
if (respObj !== null && respObj.errors) {
|
||||
this.logger.error('Add to playlist failed', song.tidalUri, resp.status, respObj.errors);
|
||||
if (resp.status === 401 || respObj.errors.some((x) => x.code === 'UNAUTHORIZED')) {
|
||||
const token = await this.refreshToken(true);
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
await this.addToPlaylistRetry(song, remaning--);
|
||||
}
|
||||
} else if (respObj === null) {
|
||||
switch (resp.status) {
|
||||
case 201:
|
||||
this.logger.info('Added to playlist', song.tidalUri, song.title);
|
||||
break;
|
||||
case 429:
|
||||
let secondsToWait = -1;
|
||||
const retryAfterHeader = resp.headers.get('Retry-After');
|
||||
if (retryAfterHeader) {
|
||||
secondsToWait = parseInt(retryAfterHeader);
|
||||
} else {
|
||||
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;
|
||||
secondsToWait = 1 + needToReplenish / replenishRateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (secondsToWait === -1) {
|
||||
this.logger.warn('Could not read headers how long to wait', resp.headers);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.logger.warn('Unknown response', resp.status, respTxt);
|
||||
break;
|
||||
if (secondsToWait === -1) {
|
||||
this.logger.warn('Could not read headers how long to wait', resp.headers);
|
||||
} else {
|
||||
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
|
||||
await sleep(secondsToWait * 1000);
|
||||
await this.addToPlaylistRetry(song, remaning--);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.logger.warn('Unknown response', resp.status, respTxt);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.logger.info(
|
||||
'Add to playlist result is neither 201 nor error',
|
||||
song.tidalUri,
|
||||
song.title,
|
||||
respObj
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.info(
|
||||
'Add to playlist result is neither 201 nor error',
|
||||
song.tidalUri,
|
||||
song.title,
|
||||
respObj
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error('Add to playlist request failed', resp?.status, e);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('Add to playlist request failed', resp?.status, e);
|
||||
}
|
||||
}
|
||||
public async addToPlaylist(song: SongInfo) {
|
||||
|
Reference in New Issue
Block a user