mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-30 18:06:15 +00:00
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.
This commit is contained in:
200
lib/Innertube.js
200
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.<string>} 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.<string>} 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.<string>} 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.<string>}
|
||||
*/
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user