refactor how youtube API requests are retried and auth errors are handled fix #48
This commit is contained in:
@ -89,6 +89,63 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist
|
|||||||
return refreshedToken;
|
return refreshedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getVideoUris(song: SongInfo, token: OauthResponse): Promise<string[]> {
|
||||||
|
this.logger.debug('getVideoUris');
|
||||||
|
|
||||||
|
if (!song.youtubeUrl) {
|
||||||
|
this.logger.info('Skip adding song to YT playlist, no youtube Url', song);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const songUrl = new URL(song.youtubeUrl);
|
||||||
|
let videoIds: string[] = [];
|
||||||
|
if (song.type === 'song') {
|
||||||
|
const youtubeId = songUrl.searchParams.get('v');
|
||||||
|
if (!youtubeId) {
|
||||||
|
this.logger.debug(
|
||||||
|
'Skip adding song to YT playlist, could not extract YT id from URL',
|
||||||
|
song.youtubeUrl
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
this.logger.debug('Found YT id from URL', song.youtubeUrl, youtubeId);
|
||||||
|
return [youtubeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumPlaylistId = songUrl.searchParams.get('list') ?? '';
|
||||||
|
const albumItemsUrl = new URL(this.apiBase + '/playlistItems');
|
||||||
|
albumItemsUrl.searchParams.append('maxResults', '50');
|
||||||
|
albumItemsUrl.searchParams.append('playlistId', albumPlaylistId);
|
||||||
|
albumItemsUrl.searchParams.append('part', 'snippet');
|
||||||
|
const albumPlaylistItemRequest = new Request(albumItemsUrl, {
|
||||||
|
headers: { Authorization: `${token.token_type} ${token.access_token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
const albumPlaylistItem = await this.fetchWithRetry<YoutubePlaylistItemResponse>(
|
||||||
|
albumPlaylistItemRequest,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
if (albumPlaylistItem === null) {
|
||||||
|
this.logger.info('Could not check album tracks');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const albumTracks = albumPlaylistItem.items ?? [];
|
||||||
|
videoIds = albumTracks.map((x) => x.snippet?.resourceId?.videoId).filter((x) => x);
|
||||||
|
this.logger.info(
|
||||||
|
'Found',
|
||||||
|
albumPlaylistItem.pageInfo?.totalResults,
|
||||||
|
'songs in album, received',
|
||||||
|
albumTracks.length
|
||||||
|
);
|
||||||
|
if (videoIds.length === 0) {
|
||||||
|
this.logger.debug(
|
||||||
|
'Skip adding album to YT playlist, is empty',
|
||||||
|
song.youtubeUrl,
|
||||||
|
albumPlaylistItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return videoIds;
|
||||||
|
}
|
||||||
public async addToPlaylist(song: SongInfo) {
|
public async addToPlaylist(song: SongInfo) {
|
||||||
this.logger.debug('addToYoutubePlaylist');
|
this.logger.debug('addToYoutubePlaylist');
|
||||||
let token = await this.refreshToken();
|
let token = await this.refreshToken();
|
||||||
@ -100,60 +157,8 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist
|
|||||||
this.logger.debug('no playlist ID configured');
|
this.logger.debug('no playlist ID configured');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!song.youtubeUrl) {
|
|
||||||
this.logger.info('Skip adding song to YT playlist, no youtube Url', song);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const songUrl = new URL(song.youtubeUrl);
|
const videoIds = await this.getVideoUris(song, token);
|
||||||
let videoIds: string[] = [];
|
|
||||||
if (song.type === 'album') {
|
|
||||||
const albumPlaylistId = songUrl.searchParams.get('list') ?? '';
|
|
||||||
const albumItemsUrl = new URL(this.apiBase + '/playlistItems');
|
|
||||||
albumItemsUrl.searchParams.append('maxResults', '50');
|
|
||||||
albumItemsUrl.searchParams.append('playlistId', albumPlaylistId);
|
|
||||||
albumItemsUrl.searchParams.append('part', 'snippet');
|
|
||||||
const albumPlaylistItemRequest = new Request(albumItemsUrl, {
|
|
||||||
headers: { Authorization: `${token.token_type} ${token.access_token}` }
|
|
||||||
});
|
|
||||||
const albumPlaylistItem = await fetch(albumPlaylistItemRequest).then((r) => r.json());
|
|
||||||
if (albumPlaylistItem.error) {
|
|
||||||
this.logger.info(
|
|
||||||
'Could not check album tracks',
|
|
||||||
albumPlaylistItem.error,
|
|
||||||
'request',
|
|
||||||
albumPlaylistItemRequest
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const albumTracks: any[] = albumPlaylistItem.items ?? [];
|
|
||||||
videoIds = albumTracks.map((x) => x.snippet?.resourceId?.videoId).filter((x) => x);
|
|
||||||
this.logger.info(
|
|
||||||
'Found',
|
|
||||||
albumPlaylistItem.pageInfo?.totalResults,
|
|
||||||
'songs in album, received',
|
|
||||||
albumTracks.length
|
|
||||||
);
|
|
||||||
this.logger.debug(videoIds);
|
|
||||||
if (videoIds.length === 0) {
|
|
||||||
this.logger.debug(
|
|
||||||
'Skip adding album to YT playlist, is empty',
|
|
||||||
song.youtubeUrl,
|
|
||||||
albumPlaylistItem
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const youtubeId = songUrl.searchParams.get('v');
|
|
||||||
if (!youtubeId) {
|
|
||||||
this.logger.debug(
|
|
||||||
'Skip adding song to YT playlist, could not extract YT id from URL',
|
|
||||||
song.youtubeUrl
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger.debug('Found YT id from URL', song.youtubeUrl, youtubeId);
|
|
||||||
videoIds.push(youtubeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let youtubeId of videoIds) {
|
for (let youtubeId of videoIds) {
|
||||||
this.logger.debug('Adding to playlist', youtubeId);
|
this.logger.debug('Adding to playlist', youtubeId);
|
||||||
@ -165,32 +170,14 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist
|
|||||||
this.logger.debug('Item not already in playlist', song.youtubeUrl, song.title);
|
this.logger.debug('Item not already in playlist', song.youtubeUrl, song.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
let retries = 3;
|
const success = await this.addSongToPlaylist(youtubeId, token);
|
||||||
let success = false;
|
if (success) {
|
||||||
while (retries > 0 && !success) {
|
|
||||||
try {
|
|
||||||
this.logger.debug('Retries', retries);
|
|
||||||
await this.addSongToPlaylist(youtubeId, token);
|
|
||||||
success = true;
|
|
||||||
this.logger.info('Added to playlist', youtubeId, song.title);
|
this.logger.info('Added to playlist', youtubeId, song.title);
|
||||||
} catch (e) {
|
|
||||||
retries--;
|
|
||||||
if (e instanceof Error && e.message === 'authError') {
|
|
||||||
this.logger.info('Refreshing auth token');
|
|
||||||
token = await this.refreshToken(true);
|
|
||||||
if (token == null) {
|
|
||||||
this.logger.error('Refreshing auth token failed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.logger.error('Add to playlist failed', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addSongToPlaylist(videoId: string, token: OauthResponse): Promise<void> {
|
private async addSongToPlaylist(videoId: string, token: OauthResponse): Promise<boolean> {
|
||||||
const addItemUrl = new URL(this.apiBase + '/playlistItems');
|
const addItemUrl = new URL(this.apiBase + '/playlistItems');
|
||||||
addItemUrl.searchParams.append('part', 'snippet');
|
addItemUrl.searchParams.append('part', 'snippet');
|
||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
@ -207,26 +194,47 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
const request = new Request(addItemUrl, options);
|
const request = new Request(addItemUrl, options);
|
||||||
const resp = await fetch(request);
|
const resp = await this.fetchWithRetry(request, token);
|
||||||
let respObj: YoutubeResponse = await resp.json();
|
return resp !== null;
|
||||||
if (respObj.error) {
|
|
||||||
this.logger.error(
|
|
||||||
'Add to playlist failed',
|
|
||||||
respObj.error.errors,
|
|
||||||
respObj.error.code,
|
|
||||||
respObj.error.message,
|
|
||||||
'request',
|
|
||||||
request
|
|
||||||
);
|
|
||||||
throw new Error(respObj.error.errors[0].reason);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isVideoInPlaylist(
|
private async fetchWithRetry<T extends YoutubeResponse>(
|
||||||
videoId: string,
|
request: Request,
|
||||||
token: OauthResponse,
|
token: OauthResponse,
|
||||||
retry: boolean = true
|
retries: number = 3
|
||||||
): Promise<boolean> {
|
): Promise<T | null> {
|
||||||
|
if (retries <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
request.headers.set('Authorization', `${token.token_type} ${token.access_token}`);
|
||||||
|
const resp = await fetch(request);
|
||||||
|
let respObj: T = await resp.json();
|
||||||
|
if (!respObj.error) {
|
||||||
|
return respObj;
|
||||||
|
}
|
||||||
|
this.logger.error(
|
||||||
|
'Request failed',
|
||||||
|
respObj.error.errors,
|
||||||
|
respObj.error.code,
|
||||||
|
respObj.error.message
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
respObj.error.code === 401 ||
|
||||||
|
respObj.error.code === 403 ||
|
||||||
|
respObj.error.errors.some((x) => x.reason === 'authError')
|
||||||
|
) {
|
||||||
|
this.logger.info('Refreshing auth token');
|
||||||
|
const newToken = await this.refreshToken(true);
|
||||||
|
if (newToken == null) {
|
||||||
|
this.logger.error('Refreshing auth token failed');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.fetchWithRetry(request, newToken, retries - 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isVideoInPlaylist(videoId: string, token: OauthResponse): Promise<boolean> {
|
||||||
const playlistItemsUrl = new URL(this.apiBase + '/playlistItems');
|
const playlistItemsUrl = new URL(this.apiBase + '/playlistItems');
|
||||||
playlistItemsUrl.searchParams.append('videoId', videoId);
|
playlistItemsUrl.searchParams.append('videoId', videoId);
|
||||||
playlistItemsUrl.searchParams.append('playlistId', YOUTUBE_PLAYLIST_ID);
|
playlistItemsUrl.searchParams.append('playlistId', YOUTUBE_PLAYLIST_ID);
|
||||||
@ -234,29 +242,15 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist
|
|||||||
let existingPlaylistItemRequest = new Request(playlistItemsUrl, {
|
let existingPlaylistItemRequest = new Request(playlistItemsUrl, {
|
||||||
headers: { Authorization: `${token.token_type} ${token.access_token}` }
|
headers: { Authorization: `${token.token_type} ${token.access_token}` }
|
||||||
});
|
});
|
||||||
let existingPlaylistItem: YoutubePlaylistItemResponse = await fetch(
|
|
||||||
existingPlaylistItemRequest
|
|
||||||
).then((r) => r.json());
|
|
||||||
if (existingPlaylistItem.error) {
|
|
||||||
this.logger.error(
|
|
||||||
'Could not check if item is already in playlist',
|
|
||||||
existingPlaylistItem.error,
|
|
||||||
'request',
|
|
||||||
existingPlaylistItemRequest
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingPlaylistItem.error.code === 401) {
|
const existingPlaylistItem = await this.fetchWithRetry<YoutubePlaylistItemResponse>(
|
||||||
const newToken = await this.refreshToken(true);
|
existingPlaylistItemRequest,
|
||||||
if (newToken === null) {
|
token
|
||||||
|
);
|
||||||
|
if (existingPlaylistItem === null) {
|
||||||
|
this.logger.error('Could not check if item is already in playlist');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (retry) {
|
|
||||||
return await this.isVideoInPlaylist(videoId, newToken, false);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return existingPlaylistItem.pageInfo && existingPlaylistItem.pageInfo.totalResults > 0;
|
return existingPlaylistItem.pageInfo && existingPlaylistItem.pageInfo.totalResults > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user