From 46a385aa06447375a0818661fc9b5b88da6dd5f2 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Mon, 9 May 2022 18:30:22 -0300 Subject: [PATCH] chore: fix major bugs and improve error handling Seems like some methods weren't working due to a typo in the browseId, this commit should fix it. Also, additional checks were added so unexpected errors aren't thrown. --- lib/Innertube.js | 200 +++++++++++++++++++++++--------------------- lib/parser/index.js | 16 ++-- lib/utils/Utils.js | 25 +++++- 3 files changed, 132 insertions(+), 109 deletions(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 975a03ee..22c2949d 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -86,48 +86,48 @@ class Innertube { /** * Notify about activity from the channels you're subscribed to. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setSubscriptions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', new_value), /** * Recommended content notifications. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setRecommendedVideos: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', new_value), /** * Notify about activity on your channel. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setChannelActivity: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', new_value), /** * Notify about replies to your comments. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setCommentReplies: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', new_value), /** * Notify when others mention your channel. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setMentions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', new_value), /** * Notify when others share your content on their channels. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setSharedContent: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', new_value) }, @@ -135,16 +135,16 @@ class Innertube { /** * If set to true, your subscriptions won't be visible to others. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', new_value), /** * If set to true, saved playlists won't appear on your channel. * - * @param {boolean} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @param {boolean} new_value - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setSavedPlaylistsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', new_value) } @@ -156,7 +156,7 @@ class Innertube { * Likes a given video. * * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ like: (video_id) => this.actions.engage('like/like', { video_id }), @@ -164,7 +164,7 @@ class Innertube { * Diskes a given video. * * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ dislike: (video_id) => this.actions.engage('like/dislike', { video_id }), @@ -172,7 +172,7 @@ class Innertube { * Removes a like/dislike. * * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ removeLike: (video_id) => this.actions.engage('like/removelike', { video_id }), @@ -181,7 +181,7 @@ class Innertube { * * @param {string} video_id * @param {string} text - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ comment: (video_id, text) => this.actions.engage('comment/create_comment', { video_id, text }), @@ -194,7 +194,7 @@ class Innertube { * @param {string} [args.video_id] * @param {string} [args.comment_id] * - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; translated_content: string; data: object; }>} */ translate: async (text, target_language, args = {}) => { const response = await this.actions.engage('comment/perform_comment_action', { @@ -210,7 +210,8 @@ class Innertube { return { success: response.success, status_code: response.status_code, - translated_content: translated_content.content + translated_content: translated_content.content, + data: response.data } }, @@ -218,7 +219,7 @@ class Innertube { * Subscribes to a given channel. * * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ subscribe: (channel_id) => this.actions.engage('subscription/subscribe', { channel_id }), @@ -226,7 +227,7 @@ class Innertube { * Unsubscribes from a given channel. * * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ unsubscribe: (channel_id) => this.actions.engage('subscription/unsubscribe', { channel_id }), @@ -236,7 +237,7 @@ class Innertube { * * @param {string} channel_id * @param {string} type PERSONALIZED | ALL | NONE - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ setNotificationPreferences: (channel_id, type) => this.actions.notifications('modify_channel_preference', { channel_id, pref: type || 'NONE' }), }; @@ -248,16 +249,16 @@ class Innertube { * @param {string} title * @param {Array.} video_ids * - * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} */ create: async (title, video_ids) => { + Utils.throwIfMissing({ title, video_ids }); const response = await this.actions.playlist('playlist/create', { title, ids: video_ids }); - if (!response.success) return response; - return { - success: true, + success: response.success, status_code: response.status_code, - playlist_id: response.data.playlistId + playlist_id: response.data.playlistId, + data: response.data } }, @@ -265,16 +266,16 @@ class Innertube { * Deletes a given playlist. * * @param {string} playlist_id - * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} */ delete: async (playlist_id) => { + Utils.throwIfMissing({ playlist_id }); const response = await this.actions.playlist('playlist/delete', { playlist_id }); - if (!response.success) return response; - return { - success: true, + success: response.success, status_code: response.status_code, - playlist_id + playlist_id: playlistId, + data: response.data } }, @@ -283,21 +284,21 @@ class Innertube { * * @param {string} playlist_id * @param {Array.} video_ids - * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} */ addVideos: async (playlist_id, video_ids) => { + Utils.throwIfMissing({ playlist_id, video_ids }); const response = await this.actions.playlist('browse/edit_playlist', { action: 'ACTION_ADD_VIDEO', playlist_id, ids: video_ids }); - if (!response.success) return response; - return { - success: true, + success: response.success, status_code: response.status_code, - playlist_id + playlist_id: playlistId, + data: response.data } }, @@ -306,9 +307,11 @@ class Innertube { * * @param {string} playlist_id * @param {Array.} video_ids - * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + * + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} */ removeVideos: async (playlist_id, video_ids) => { + Utils.throwIfMissing({ playlist_id, video_ids }); const plinfo = await this.actions.browse(`VL${playlist_id}`); const list = Utils.findNode(plinfo.data, 'contents', 'contents', 13, false); @@ -322,13 +325,12 @@ class Innertube { playlist_id, ids: set_video_ids }); - - if (!response.success) return response; - + return { - success: true, + success: response.success, status_code: response.status_code, - playlist_id + playlist_id: playlist_id, + data: response.data } } }; @@ -340,26 +342,32 @@ class Innertube { * @param {string} setting_id * @param {string} type * @param {string} new_value - * @returns {Promise.<{ success: boolean; status_code: string; }>} + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ async #setSetting(setting_id, type, new_value) { + Utils.throwIfMissing({ setting_id, type, new_value }); + + const values = { ON: true, OFF: false }; + + if (!values.hasOwnProperty(new_value)) + throw new Utils.InnertubeError('Invalid option', { option: new_value, available_options: Object.keys(values) }); + const response = await this.actions.browse(type); - if (!response.success) return response; - + const contents = ({ - account_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options, - account_privacy: () => Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options + SPaccount_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options, + SPaccount_privacy: () => Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options })[type.trim()](); const option = contents.find((option) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id); const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId; - const set_setting = await this.actions.account('account/set_setting', { new_value: type == 'account_privacy' ? !new_value : new_value, setting_item_id }); + const set_setting = await this.actions.account('account/set_setting', { + new_value: type == 'SPaccount_privacy' ? !values[new_value] : values[new_value], + setting_item_id + }); - return { - success: set_setting.success, - status_code: set_setting.status_code, - } + return set_setting; } /** @@ -413,8 +421,6 @@ class Innertube { */ async getAccountInfo() { const response = await this.actions.account('account/account_menu'); - if (!response.success) throw new Utils.InnertubeError('Could not get account info', response); - const menu = Utils.findNode(response, 'actions', 'multiPageMenuRenderer', 6, false); return { @@ -431,19 +437,22 @@ class Innertube { * @param {string} query - search query. * @param {object} options - search options. * @param {string} options.client - client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`. - * @param {string} options.period - filter videos uploaded within a period, can be: any | hour | day | week | month | year * @param {string} options.order - filter results by order, can be: relevance | rating | age | views + * @param {string} options.period - filter videos uploaded within a period, can be: any | hour | day | week | month | year * @param {string} options.duration - filter video results by duration, can be: any | short | long * * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } | * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>} */ - async search(query, options = { client: 'YOUTUBE', period: 'any', order: 'relevance', duration: 'any' }) { - const response = await this.actions.search({ query, options, is_ytm: options.client == 'YTMUSIC' }); + async search(query, options) { + Utils.throwIfMissing({ query }); + + const final_options = Object.assign({ client: 'YOUTUBE', period: 'any', duration: 'any', order: 'relevance' }, options); + const response = await this.actions.search({ query, options: final_options, is_ytm: final_options.client == 'YTMUSIC' }); const results = new Parser(this, response.data, { query, - client: options.client, + client: final_options.client, data_type: 'SEARCH' }).parse(); @@ -460,8 +469,9 @@ class Innertube { * @returns {Promise.<[{ text: string; bold_text: string }]>} */ async getSearchSuggestions(input, options = { client: 'YOUTUBE' }) { + Utils.throwIfMissing({ input }); + const response = await this.actions.getSearchSuggestions(options.client, input); - if (!response.success) throw new Utils.InnertubeError('Could not get search suggestions', response); if (options.client === 'YTMUSIC' && !response.data.contents) return []; const suggestions = new Parser(this, response.data, { @@ -480,12 +490,12 @@ class Innertube { * @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>} */ async getDetails(video_id) { - if (!video_id) throw new Utils.MissingParamError('Video id is missing'); - + Utils.throwIfMissing({ video_id }); + const response = await this.actions.getVideoInfo(video_id); const continuation = await this.actions.next({ video_id }); - continuation.success && (response.continuation = continuation.data); - + response.continuation = continuation.data; + const details = new Parser(this, response, { client: 'YOUTUBE', data_type: 'VIDEO_INFO' @@ -516,13 +526,13 @@ class Innertube { * @return {Promise.<{ page_count: number; comment_count: number; items: []; }>} */ async getComments(video_id, sort_by) { + Utils.throwIfMissing({ video_id }); + const payload = Proto.encodeCommentsSectionParams(video_id, { sort_by: sort_by || 'TOP_COMMENTS' }); const response = await this.actions.next({ ctoken: payload }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve comments', response); - const comments = new Parser(this, response.data, { video_id, client: 'YOUTUBE', @@ -539,9 +549,10 @@ class Innertube { * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} */ async getChannel(id) { + Utils.throwIfMissing({ id }); + const response = await this.actions.browse(id); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve channel info.', response); - + const channel_info = new Parser(this, response.data, { client: 'YOUTUBE', data_type: 'CHANNEL' @@ -556,8 +567,7 @@ class Innertube { */ async getHistory() { const response = await this.actions.browse('FEhistory'); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve watch history', response); - + const history = new Parser(this, response, { client: 'YOUTUBE', data_type: 'HISTORY' @@ -572,8 +582,7 @@ class Innertube { */ async getHomeFeed() { const response = await this.actions.browse('FEwhat_to_watch'); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response); - + const homefeed = new Parser(this, response, { client: 'YOUTUBE', data_type: 'HOMEFEED' @@ -591,8 +600,7 @@ class Innertube { */ async getTrending() { const response = await this.actions.browse('FEtrending'); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve trending content', response); - + const trending = new Parser(this, response, { client: 'YOUTUBE', data_type: 'TRENDING' @@ -606,8 +614,7 @@ class Innertube { */ async getLibrary() { const response = await this.actions.browse('FElibrary'); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve library', response); - + const library = new Parser(this, response.data, { client: 'YOUTUBE', data_type: 'LIBRARY' @@ -622,8 +629,7 @@ class Innertube { */ async getSubscriptionsFeed() { const response = this.actions.browse('FEsubscriptions'); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve subscriptions feed', response); - + const subsfeed = new Parser(this, response, { client: 'YOUTUBE', data_type: 'SUBSFEED' @@ -638,8 +644,7 @@ class Innertube { */ async getNotifications() { const response = await this.actions.notifications('get_notification_menu'); - if (!response.success) throw new Utils.InnertubeError('Could not fetch notifications', response); - + const notifications = new Parser(this, response.data, { client: 'YOUTUBE', data_type: 'NOTIFICATIONS' @@ -654,7 +659,6 @@ class Innertube { */ async getUnseenNotificationsCount() { const response = await this.actions.notifications('get_unseen_count'); - if (!response.success) throw new Utils.InnertubeError('Could not get unseen notifications count', response); return response.data.unseenCount; } @@ -665,13 +669,13 @@ class Innertube { * @returns {Promise.} */ async getLyrics(video_id) { + Utils.throwIfMissing({ video_id }); + const continuation = await this.actions.next({ video_id: video_id, is_ytm: true }); - if (!continuation.success) throw new Utils.InnertubeError('Could not retrieve lyrics', continuation); - const lyrics_tab = Utils.findNode(continuation, 'contents', 'Lyrics', 8, false); const response = await this.actions.browse(lyrics_tab.endpoint?.browseEndpoint.browseId, { is_ytm: true }); - if (!response.success || !response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id }); + if (!response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id }); const lyrics = Utils.findNode(response.data, 'contents', 'runs', 6, false); return lyrics.runs[0].text; @@ -688,9 +692,9 @@ class Innertube { * { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>} */ async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) { + Utils.throwIfMissing({ playlist_id }); + const response = await this.actions.browse(`VL${playlist_id}`, { is_ytm: options.client == 'YTMUSIC' }); - if (!response.success) throw new Utils.InnertubeError('Could not get playlist', response); - const playlist = new Parser(this, response.data, { client: options.client, data_type: 'PLAYLIST' @@ -765,7 +769,7 @@ class Innertube { * An alternative to {@link download}. * Returns deciphered streaming data. * - * @param {string} id - video id + * @param {string} video_id - video id * @param {object} options - download options. * @param {string} options.quality - video quality; 360p, 720p, 1080p, etc... * @param {string} options.type - download type, can be: video, audio or videoandaudio @@ -773,12 +777,14 @@ class Innertube { * * @returns {Promise.<{ selected_format: {}; formats: [] }>} */ - async getStreamingData(id, options = {}) { + async getStreamingData(video_id, options = {}) { + Utils.throwIfMissing({ video_id }); + options.quality = options.quality || '360p'; options.type = options.type || 'videoandaudio'; options.format = options.format || 'mp4'; - const data = await this.actions.getVideoInfo(id); + const data = await this.actions.getVideoInfo(video_id); const streaming_data = this.#chooseFormat(options, data); if (!streaming_data.selected_format) @@ -790,7 +796,7 @@ class Innertube { /** * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. * - * @param {string} id - video id + * @param {string} video_id - video id * @param {object} options - download options. * @param {string} [options.quality] - video quality; 360p, 720p, 1080p, etc... * @param {string} [options.type] - download type, can be: video, audio or videoandaudio @@ -798,9 +804,9 @@ class Innertube { * * @return {Stream.PassThrough} */ - download(id, options = {}) { - if (!id) throw new Utils.MissingParamError('Video id is missing'); - + download(video_id, options = {}) { + Utils.throwIfMissing({ video_id }); + options.quality = options.quality || '360p'; options.type = options.type || 'videoandaudio'; options.format = options.format || 'mp4'; @@ -811,7 +817,7 @@ class Innertube { const cpn = Utils.generateRandomString(16); const stream = new Stream.PassThrough(); - this.actions.getVideoInfo(id, cpn).then(async (video_data) => { + this.actions.getVideoInfo(video_id, cpn).then(async (video_data) => { if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED') return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status }); if (!video_data.streamingData) diff --git a/lib/parser/index.js b/lib/parser/index.js index 3a7ad47d..47ab6464 100644 --- a/lib/parser/index.js +++ b/lib/parser/index.js @@ -69,8 +69,7 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.search({ ctoken, is_ytm: false }); - if (!response.success) throw new Utils.InnertubeError('Could not get continuation', response); - + const continuation_items = Utils.findNode(response.data, 'onResponseReceivedCommands', 'itemSectionRenderer', 4, false); return parseItems(continuation_items); }; @@ -363,8 +362,7 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); - + return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); } @@ -427,8 +425,7 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); - + const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false); subsfeed.items = []; @@ -502,8 +499,7 @@ class Parser { const ctoken = citem?.continuationItemRenderer?.continuationEndpoint?.getNotificationMenuEndpoint?.ctoken; const response = await this.session.actions.notifications('get_notification_menu', { ctoken }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); - + return parseItems(response.data.actions[0].appendContinuationItemsAction.continuationItems); } @@ -536,7 +532,6 @@ class Parser { const params = tab_renderer.endpoint.browseEndpoint.params; categories[category_title].getVideos = async () => { const response = await this.session.actions.browse('FEtrending', { params }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve category videos', response); const tabs = Utils.findNode(response, 'contents', 'tabRenderer', 4, false); const tab = tabs.find((tab) => tab.tabRenderer.title === tab_renderer.title); @@ -578,8 +573,7 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); - + history.items = []; return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index 9543ca23..9a5aca70 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -134,6 +134,29 @@ function camelToSnake(string) { return string[0].toLowerCase() + string.slice(1, string.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } +/** + * Checks if a given client is valid. + * + * @param {string} client + * @returns {boolean} + */ +function isValidClient(client) { + return [ 'YOUTUBE', 'YTMUSIC' ].includes(client); +} + +/** + * Throws an error if given parameters are undefined. + * + * @param {object} params + * @returns + */ +function throwIfMissing(params) { + for (const [key, value] of Object.entries(params)) { + if (!value) + throw new MissingParamError(`${key} is missing`); + } +} + /** * Turns the ntoken transform data into a valid json array * @@ -151,6 +174,6 @@ function refineNTokenData(data) { } const errors = { InnertubeError, UnavailableContentError, ParsingError, DownloadError, MissingParamError, NoStreamingDataError }; -const functions = { findNode, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData }; +const functions = { findNode, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings, camelToSnake, isValidClient, throwIfMissing, timeToSeconds, refineNTokenData }; module.exports = { ...functions, ...errors }; \ No newline at end of file