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 {
|
try {
|
||||||
let albumUrl: URL;
|
let albumUrl: URL;
|
||||||
if (nextLink) {
|
if (nextLink) {
|
||||||
|
this.logger.debug('getAlbumItems nextPage', nextLink);
|
||||||
albumUrl = new URL(nextLink);
|
albumUrl = new URL(nextLink);
|
||||||
} else {
|
} else {
|
||||||
|
this.logger.debug(
|
||||||
|
'getAlbumItems albumUrl',
|
||||||
|
`${this.apiBase}/albums/${albumId}/relationships/items`
|
||||||
|
);
|
||||||
albumUrl = new URL(`${this.apiBase}/albums/${albumId}/relationships/items`);
|
albumUrl = new URL(`${this.apiBase}/albums/${albumId}/relationships/items`);
|
||||||
albumUrl.searchParams.append('countryCode', 'DE');
|
albumUrl.searchParams.append('countryCode', 'DE');
|
||||||
}
|
}
|
||||||
@ -144,7 +149,12 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
|
|||||||
return { id: x.id, type: x.type };
|
return { id: x.id, type: x.type };
|
||||||
});
|
});
|
||||||
if (respData.links?.next) {
|
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);
|
tracks = tracks.concat(nextPage);
|
||||||
}
|
}
|
||||||
return tracks;
|
return tracks;
|
||||||
@ -243,96 +253,108 @@ export class TidalPlaylistAdder extends OauthPlaylistAdder implements PlaylistAd
|
|||||||
this.logger.error('Could not check for tidal dupes', dbe);
|
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
|
// Tidal can only handle max. 20 items to be added at once
|
||||||
const options: RequestInit = {
|
// This isn't documented, but the API helpfully provides a useful error message
|
||||||
method: 'POST',
|
let chunkSize = 20;
|
||||||
headers: {
|
const chunkedTracks: { id: string; type: string }[][] = [];
|
||||||
Authorization: `${token.token_type} ${token.access_token}`,
|
let chunkIndex = 0;
|
||||||
'Content-Type': 'application/vnd.api+json',
|
while (chunkIndex < tracks.length) {
|
||||||
Accept: 'application/vnd.api+json'
|
chunkedTracks.push(tracks.slice(chunkIndex, chunkIndex + chunkSize));
|
||||||
},
|
chunkIndex += chunkSize;
|
||||||
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);
|
|
||||||
|
|
||||||
|
const apiUrl = new URL(`${this.apiBase}/playlists/${TIDAL_PLAYLIST_ID}/relationships/items`);
|
||||||
|
let options: RequestInit;
|
||||||
|
let request: Request;
|
||||||
let resp: Response | null = null;
|
let resp: Response | null = null;
|
||||||
let respTxt: string | null = null;
|
let respTxt: string | null = null;
|
||||||
try {
|
for (let chunk of chunkedTracks) {
|
||||||
resp = await fetch(request);
|
options = {
|
||||||
this.processTidalHeaders(resp.headers);
|
method: 'POST',
|
||||||
let respObj: TidalAddToPlaylistResponse | null = null;
|
headers: {
|
||||||
// If the request was successful, a 201 with no content is received
|
Authorization: `${token.token_type} ${token.access_token}`,
|
||||||
// Errors will have content and a different status code
|
'Content-Type': 'application/vnd.api+json',
|
||||||
if (resp.status !== 201 && resp.status !== 429) {
|
Accept: 'application/vnd.api+json'
|
||||||
respObj = await resp.json();
|
},
|
||||||
} else {
|
body: JSON.stringify({
|
||||||
respTxt = await resp.text();
|
data: chunk,
|
||||||
}
|
meta: {
|
||||||
if (respObj !== null && respObj.errors) {
|
positionBefore: 'ffb6286e-237a-4dfc-bbf1-2fb0eb004ed5' // Hardcoded last element of list
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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) {
|
if (respObj !== null && respObj.errors) {
|
||||||
switch (resp.status) {
|
this.logger.error('Add to playlist failed', song.tidalUri, resp.status, respObj.errors);
|
||||||
case 201:
|
if (resp.status === 401 || respObj.errors.some((x) => x.code === 'UNAUTHORIZED')) {
|
||||||
this.logger.info('Added to playlist', song.tidalUri, song.title);
|
const token = await this.refreshToken(true);
|
||||||
break;
|
if (token == null) {
|
||||||
case 429:
|
return;
|
||||||
let secondsToWait = -1;
|
}
|
||||||
const retryAfterHeader = resp.headers.get('Retry-After');
|
await this.addToPlaylistRetry(song, remaning--);
|
||||||
if (retryAfterHeader) {
|
}
|
||||||
secondsToWait = parseInt(retryAfterHeader);
|
} else if (respObj === null) {
|
||||||
} else {
|
switch (resp.status) {
|
||||||
const remainingTokens = resp.headers.get('x-ratelimit-remaining');
|
case 201:
|
||||||
const requiredTokens = resp.headers.get('x-ratelimit-requested-tokens');
|
this.logger.info('Added to playlist', song.tidalUri, song.title);
|
||||||
const replenishRate = resp.headers.get('x-ratelimit-replenish-rate');
|
break;
|
||||||
if (remainingTokens !== null && requiredTokens !== null && replenishRate !== null) {
|
case 429:
|
||||||
const remainingTokensValue = parseInt(remainingTokens);
|
let secondsToWait = -1;
|
||||||
const requiredTokensValue = parseInt(requiredTokens);
|
const retryAfterHeader = resp.headers.get('Retry-After');
|
||||||
const replenishRateValue = parseInt(replenishRate);
|
if (retryAfterHeader) {
|
||||||
const needToReplenish = requiredTokensValue - remainingTokensValue;
|
secondsToWait = parseInt(retryAfterHeader);
|
||||||
secondsToWait = 1 + needToReplenish / replenishRateValue;
|
} 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) {
|
||||||
if (secondsToWait === -1) {
|
this.logger.warn('Could not read headers how long to wait', resp.headers);
|
||||||
this.logger.warn('Could not read headers how long to wait', resp.headers);
|
} else {
|
||||||
} else {
|
this.logger.warn(
|
||||||
this.logger.warn(
|
'Received HTTP 429 Too Many Requests. Retrying in',
|
||||||
'Received HTTP 429 Too Many Requests. Retrying in',
|
secondsToWait,
|
||||||
secondsToWait,
|
'sec'
|
||||||
'sec'
|
);
|
||||||
);
|
// Try again secondsToWait sec later, just to be safe one additional second
|
||||||
// Try again secondsToWait sec later, just to be safe one additional second
|
await sleep(secondsToWait * 1000);
|
||||||
setTimeout(() => {
|
await this.addToPlaylistRetry(song, remaning--);
|
||||||
this.addToPlaylistRetry(song, remaning--);
|
}
|
||||||
}, secondsToWait * 1000);
|
break;
|
||||||
}
|
default:
|
||||||
break;
|
this.logger.warn('Unknown response', resp.status, respTxt);
|
||||||
default:
|
break;
|
||||||
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 {
|
} catch (e) {
|
||||||
this.logger.info(
|
this.logger.error('Add to playlist request failed', resp?.status, e);
|
||||||
'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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async addToPlaylist(song: SongInfo) {
|
public async addToPlaylist(song: SongInfo) {
|
||||||
|
Reference in New Issue
Block a user