diff --git a/src/lib/server/playlist/ytPlaylistAdder.ts b/src/lib/server/playlist/ytPlaylistAdder.ts index dfcdc6d..828d227 100644 --- a/src/lib/server/playlist/ytPlaylistAdder.ts +++ b/src/lib/server/playlist/ytPlaylistAdder.ts @@ -89,6 +89,63 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist return refreshedToken; } + private async getVideoUris(song: SongInfo, token: OauthResponse): Promise { + 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( + 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) { this.logger.debug('addToYoutubePlaylist'); let token = await this.refreshToken(); @@ -100,60 +157,8 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist this.logger.debug('no playlist ID configured'); return; } - 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 === '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); - } + const videoIds = await this.getVideoUris(song, token); for (let youtubeId of videoIds) { 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); } - let retries = 3; - let success = false; - 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); - } 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); - } - } + const success = await this.addSongToPlaylist(youtubeId, token); + if (success) { + this.logger.info('Added to playlist', youtubeId, song.title); } } } - private async addSongToPlaylist(videoId: string, token: OauthResponse): Promise { + private async addSongToPlaylist(videoId: string, token: OauthResponse): Promise { const addItemUrl = new URL(this.apiBase + '/playlistItems'); addItemUrl.searchParams.append('part', 'snippet'); const options: RequestInit = { @@ -207,26 +194,47 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist }) }; const request = new Request(addItemUrl, options); - const resp = await fetch(request); - let respObj: YoutubeResponse = await resp.json(); - 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); - } + const resp = await this.fetchWithRetry(request, token); + return resp !== null; } - private async isVideoInPlaylist( - videoId: string, + private async fetchWithRetry( + request: Request, token: OauthResponse, - retry: boolean = true - ): Promise { + retries: number = 3 + ): Promise { + 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 { const playlistItemsUrl = new URL(this.apiBase + '/playlistItems'); playlistItemsUrl.searchParams.append('videoId', videoId); playlistItemsUrl.searchParams.append('playlistId', YOUTUBE_PLAYLIST_ID); @@ -234,28 +242,14 @@ export class YoutubePlaylistAdder extends OauthPlaylistAdder implements Playlist let existingPlaylistItemRequest = new Request(playlistItemsUrl, { 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 newToken = await this.refreshToken(true); - if (newToken === null) { - return false; - } - if (retry) { - return await this.isVideoInPlaylist(videoId, newToken, false); - } else { - return false; - } - } + const existingPlaylistItem = await this.fetchWithRetry( + existingPlaylistItemRequest, + token + ); + if (existingPlaylistItem === null) { + this.logger.error('Could not check if item is already in playlist'); + return false; } return existingPlaylistItem.pageInfo && existingPlaylistItem.pageInfo.totalResults > 0; }