Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

youtubei.js

Package Overview
Dependencies
Maintainers
1
Versions
124
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

youtubei.js - npm Package Compare versions

Comparing version 1.4.1 to 1.4.2-d.1

lib/core/SessionBuilder.js

839

lib/core/Actions.js

@@ -9,430 +9,487 @@ 'use strict';

/**
* Performs direct interactions on YouTube.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} engagement_type - Type of engagement.
* @param {object} args - Engagement arguments.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function engage(session, engagement_type, args = {}) {
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
const data = { context: session.context };
switch (engagement_type) {
case 'like/like':
case 'like/dislike':
case 'like/removelike':
data.target = {
videoId: args.video_id
}
break;
case 'subscription/subscribe':
case 'subscription/unsubscribe':
data.channelIds = [args.channel_id];
data.params = engagement_type == 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
break;
case 'comment/create_comment':
data.commentText = args.text;
data.createCommentParams = Proto.encodeCommentParams(args.video_id);
break;
case 'comment/create_comment_reply':
data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id);
data.commentText = args.text;
break;
case 'comment/perform_comment_action':
const action = ({
like: () => Proto.encodeCommentActionParams(5, args.comment_id, args.video_id, args.channel_id),
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id, args.channel_id),
})[args.comment_action]();
data.actions = [ action ];
break;
case 'playlist/create':
data.title = args.title;
data.videoIds = [args.video_id];
break;
case 'playlist/delete':
data.playlistId = args.playlist_id;
break;
case 'browse/edit_playlist':
data.playlistId = args.playlist_id;
data.actions = args.video_ids.map((id) => ({
action: args.action,
addedVideoId: id
}));
break;
default:
throw new Utils.InnertubeError('Invalid action', engagement_type);
class Actions {
#session;
#request;
constructor(session) {
this.#session = session;
this.#request = session.request;
}
const response = await session.YTRequester.post(`/${engagement_type}`, JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
return {
success: true,
status_code: response.status
};
}
/**
* Accesses YouTube's various sections.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action argumenets.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function browse(session, action, args = {}) {
if (!session.logged_in && ![ 'home_feed', 'lyrics',
'music_playlist', 'playlist', 'trending' ].includes(action))
/**
* Covers Innertube's browse endpoint, mostly used to
* access YouTube's sections such as the home page
* and sometimes to retrieve continuations.
*
* @param {string} id - browseId or a continuation token
* @param {object} args - additional arguments
* @param {string} [args.params]
* @param {boolean} [args.is_ytm]
* @param {boolean} [args.is_ctoken]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object }>}
*/
async browse(id, args = {}) {
if (this.#needsLogin(id) && !this.#session.logged_in)
throw new Utils.InnertubeError('You are not signed in');
const data = { context: this.#session.context };
args.params &&
(data.params = args.params);
args.is_ctoken &&
(data.continuation = id) ||
(data.browseId = id);
if (args.is_ytm) {
const context = JSON.parse(JSON.stringify(this.#session.context)); // deep copy the context obj so we don't accidentally change it
const data = { context: session.context };
switch (action) {
case 'account_notifications':
data.browseId = 'SPaccount_notifications';
break;
case 'account_privacy':
data.browseId = 'SPaccount_privacy';
break;
case 'history':
data.browseId = 'FEhistory';
break;
case 'home_feed':
data.browseId = 'FEwhat_to_watch';
break;
case 'trending':
data.browseId = 'FEtrending';
args.params && (data.params = args.params);
break;
case 'subscriptions_feed':
data.browseId = 'FEsubscriptions';
break;
case 'lyrics':
case 'music_playlist':
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
data.context = context;
data.browseId = args.browse_id;
break;
case 'channel':
case 'playlist':
data.browseId = args.browse_id;
break;
case 'continuation':
data.continuation = args.ctoken;
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post('/browse', JSON.stringify(data));
return response;
}
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
const response = await requester.post('/browse', JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
/**
* Covers endpoints used to perform direct interactions
* on YouTube.
*
* @param {string} action
* @param {object} args
* @param {string} [args.video_id]
* @param {string} [args.channel_id]
* @param {string} [args.comment_id]
* @param {string} [args.comment_action]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async engage(action, args = {}) {
if (!this.#session.logged_in && !args.hasOwnProperty('text'))
throw new Utils.InnertubeError('You are not signed in');
const data = { context: this.#session.context };
switch (action) {
case 'like/like':
case 'like/dislike':
case 'like/removelike':
data.target = {
videoId: args.video_id
}
break;
case 'subscription/subscribe':
case 'subscription/unsubscribe':
data.channelIds = [args.channel_id];
data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
break;
case 'comment/create_comment':
data.commentText = args.text;
data.createCommentParams = Proto.encodeCommentParams(args.video_id);
break;
case 'comment/create_comment_reply':
data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id);
data.commentText = args.text;
break;
case 'comment/perform_comment_action':
const params = {
video_id: args.video_id,
comment_id: args.comment_id
};
const target_action = ({
like: () => Proto.encodeCommentActionParams(5, params),
dislike: () => Proto.encodeCommentActionParams(4, params),
translate: () => {
params.text = args.text;
params.target_language = args.target_language;
return Proto.encodeCommentActionParams(22, params);
}
})[args.comment_action]();
data.actions = [ target_action ];
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post(`/${action}`, JSON.stringify(data));
return response;
}
/**
* Covers endpoints related to account management.
*
* @param {string} action
* @param {object} args
* @param {string} args.new_value
* @param {string} args.setting_item_id
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object }>}
*/
async account(action, args = {}) {
if (!this.#session.logged_in)
throw new Utils.InnertubeError('You are not signed in');
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Account settings endpoints.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action argumenets.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function account(session, action, args = {}) {
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
const data = {};
switch (action) {
case 'account/account_menu':
data.context = session.context;
break;
case 'account/set_setting':
data.context = session.context;
const data = { context: this.#session.context };
if (action === 'account/set_setting') {
data.newValue = { boolValue: args.new_value };
data.settingItemId = args.setting_item_id;
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post(`/${action}`, JSON.stringify(data));
return response;
}
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Accesses YouTube Music endpoints (/youtubei/v1/music/).
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action arguments.
* @todo Implement more actions.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function music(session, action, args) {
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
let data = {};
switch (action) {
case 'get_search_suggestions':
data.context = context;
data.input = args.input || '';
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await session.YTMRequester.post(`/music/${action}`, JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Searches a given query on YouTube/YTMusic.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} client - YouTube client: YOUTUBE | YTMUSIC
* @param {object} args - Search arguments.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function search(session, client, args = {}) {
const data = { context: session.context };
switch (client) {
case 'YOUTUBE':
if (args.query) {
/**
* Covers endpoint used for searches.
*
* @param {object} args
* @param {string} args.query
* @param {object} args.options
* @param {string} args.options.period
* @param {string} args.options.duration
* @param {string} args.options.order
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async search(args = {}) {
const data = { context: this.#session.context };
if (args.hasOwnProperty('query')) {
data.query = args.query;
if (!args.is_ytm) {
data.params = Proto.encodeSearchFilter(args.options.period, args.options.duration, args.options.order);
data.query = args.query;
} else {
data.continuation = args.ctoken;
}
break;
case 'YTMUSIC':
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
} else {
data.continuation = args.ctoken;
}
if (args.is_ytm) {
const context = JSON.parse(JSON.stringify(this.#session.context));
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
data.context = context;
data.query = args.query;
break;
default:
throw new Utils.InnertubeError('Invalid client', client);
}
const response = await this.#request.post('/search', JSON.stringify(data));
return response;
}
const requester = client == 'YOUTUBE' && session.YTRequester || session.YTMRequester;
const response = await requester.post('/search', JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
/**
* Endpoint used fo Shorts' sound search.
*
* @param {object} args
* @param {string} args.query
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async searchSound(args = {}) {
const context = JSON.parse(JSON.stringify(this.#session.context)); // deep copy the context obj so we don't accidentally change it
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Interacts with YouTube's notification system.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action arguments.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function notifications(session, action, args = {}) {
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
const data = {};
switch (action) {
case 'modify_channel_preference':
const pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
data.context = session.context;
data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]);
break;
case 'get_notification_menu':
data.context = session.context;
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
args.ctoken && (data.ctoken = args.ctoken);
break;
case 'get_unseen_count':
data.context = session.context;
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
context.client.clientVersion = Constants.CLIENTS.ANDROID.VERSION;
context.client.clientName = Constants.CLIENTS.ANDROID.NAME;
const data = { context };
data.query = args.query;
const response = await this.#request.post('/sfv/search', JSON.stringify(data));
return response;
}
const response = await session.YTRequester.post(`/notification/${action}`, JSON.stringify(data)).catch((err) => err);
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
if (action === 'modify_channel_preference') return { success: true, status_code: response.status };
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Interacts with YouTube's livechat system.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action arguments.
* @returns {Promise.<{ success: boolean; data: object; message?: string }>}
*/
async function livechat(session, action, args = {}) {
const data = {};
switch (action) {
case 'live_chat/get_live_chat':
data.context = session.context;
data.continuation = args.ctoken;
break;
case 'live_chat/send_message':
data.context = session.context;
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
data.clientMessageId = `ytjs-${Uuid.v4()}`;
data.richMessage = {
textSegments: [{ text: args.text }]
}
break;
case 'live_chat/get_item_context_menu':
data.context = session.context;
break;
case 'live_chat/moderate':
data.context = session.context;
data.params = args.cmd_params;
break;
case 'updated_metadata':
data.context = session.context;
data.videoId = args.video_id;
args.continuation && (data.continuation = args.continuation);
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
/**
* Covers endpoints used for playlist management.
*
* @param {string} action
* @param {object} args
* @param {string} [args.title]
* @param {string} [args.ids]
* @param {string} [args.playlist_id]
* @param {string} [args.action]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async playlist(action, args = {}) {
if (!this.#session.logged_in)
throw new Utils.InnertubeError('You are not signed in');
const data = { context: this.#session.context };
switch (action) {
case 'playlist/create':
data.title = args.title;
data.videoIds = args.ids;
break;
case 'playlist/delete':
data.playlistId = args.playlist_id;
break;
case 'browse/edit_playlist':
data.playlistId = args.playlist_id;
data.actions = args.ids.map((id) => ({
'ACTION_ADD_VIDEO': {
action: args.action,
addedVideoId: id
},
'ACTION_REMOVE_VIDEO': {
action: args.action,
setVideoId: id
}
})[args.action]);
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post(`/${action}`, JSON.stringify(data));
return response;
}
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((err) => err);
if (response instanceof Error) return { success: false, message: response.message };
/**
* Covers endpoints used for notifications management.
*
* @param {string} action
* @param {object} args
* @param {string} [args.pref]
* @param {string} [args.channel_id]
* @param {string} [args.ctoken]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async notifications(action, args) {
if (!this.#session.logged_in)
throw new Utils.InnertubeError('You are not signed in');
const data = { context: this.#session.context };
switch (action) {
case 'modify_channel_preference':
const pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]);
break;
case 'get_notification_menu':
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
args.ctoken && (data.ctoken = args.ctoken);
break;
case 'get_unseen_count':
// doesn't require any parameter
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
return { success: true, data: response.data };
}
const response = await this.#request.post(`/notification/${action}`, JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.status_code || 0, message: response.message };
return response;
}
/**
* Covers livechat endpoints.
*
* @param {string} action
* @param {object} args
* @param {string} [args.text]
* @param {string} [args.video_id]
* @param {string} [args.channel_id]
* @param {string} [args.ctoken]
* @param {string} [args.params]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async livechat(action, args = {}) {
const data = { context: this.#session.context };
switch (action) {
case 'live_chat/get_live_chat':
data.continuation = args.ctoken;
break;
case 'live_chat/send_message':
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
data.clientMessageId = Uuid.v4();
data.richMessage = {
textSegments: [{ text: args.text }]
}
break;
case 'live_chat/get_item_context_menu':
// note: this is currently broken due to a recent refactor
break;
case 'live_chat/moderate':
data.params = args.params;
break;
case 'updated_metadata':
data.videoId = args.video_id;
args.ctoken && (data.continuation = args.ctoken);
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post(`/${action}`, JSON.stringify(data));
return response;
}
/**
* Covers endpoints used to report content.
*
* @param {string} action
* @param {object} args
* @param {object} [args.action]
* @param {string} [args.params]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async flag(action, args) {
if (!this.#session.logged_in)
throw new Utils.InnertubeError('You are not signed in');
const data = { context: this.#session.context };
switch (action) {
case 'flag/flag':
data.action = args.action;
break;
case 'flag/get_form':
data.params = args.params;
break;
default:
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await this.#request.post(`/${action}`, JSON.stringify(data));
return response;
}
/**
* Covers specific YouTube Music endpoints.
*
* @param {string} action
* @param {string} args.input
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async music(action, args) {
const context = JSON.parse(JSON.stringify(this.#session.context)); // deep copy the context obj so we don't accidentally change it
/**
* Requests continuation for previously performed actions.
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Continuation arguments.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function next(session, args = {}) {
let data = { context: session.context };
args.continuation_token && (data.continuation = args.continuation_token);
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
const data = { context };
data.input = args.input || '';
const response = await this.#request.post(`/music/${action}`, JSON.stringify(data));
return response;
}
/**
* Mostly used to retrieve data continuation for
* previously executed actions.
*
* @param {string} action
* @param {object} args
* @param {string} args.video_id
* @param {string} args.channel_id
* @param {string} args.ctoken
* @param {boolean} is_ytm
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async next(args = {}) {
const data = { context: this.#session.context };
args.ctoken &&
(data.continuation = args.ctoken);
args.video_id &&
(data.videoId = args.video_id);
if (args.is_ytm) {
const context = JSON.parse(JSON.stringify(this.#session.context)); // deep copy the context obj so we don't accidentally change it
if (args.video_id) {
data.videoId = args.video_id;
if (args.ytmusic) {
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
data.context = context;
data.isAudioOnly = true;
data.tunerSettingValue = 'AUTOMIX_SETTING_NORMAL';
} else {
data.racyCheckOk = true;
data.contentCheckOk = false;
data.autonavState = 'STATE_NONE';
data.playbackContext = { vis: 0, lactMilliseconds: '-1' };
data.captionsRequested = false;
}
const response = await this.#request.post('/next', JSON.stringify(data));
return response;
}
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
const response = await requester.post('/next', JSON.stringify(data)).catch((error) => error);
if (response instanceof Error) return {
success: false,
status_code: response.response?.status || 0,
message: response.message
};
return {
success: true,
status_code: response.status,
data: response.data
};
}
/**
* Retrieves video data.
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Request arguments.
* @returns {Promise.<object>} - Video data.
*/
async function getVideoInfo(session, args = {}) {
const response = await session.YTRequester.post(`/player`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context))).catch((err) => err);
if (response instanceof Error) throw new Utils.InnertubeError(`Could not get video info: ${response.message}`);
return response.data;
}
/**
* Gets search suggestions.
*
* @param {Innertube} session - A valid innertube session
* @param {string} query - Search query
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function getSearchSuggestions(session, client, input) {
if (!['YOUTUBE', 'YTMUSIC'].includes(client))
throw new Utils.InnertubeError('Invalid client', client);
/**
* Used to retrieve video info.
*
* @param {string} id
* @param {string} [cpn]
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async getVideoInfo(id, cpn) {
const data = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
referer: 'https://www.youtube.com',
currentUrl: '/watch?v=' + id,
autonavState: 'STATE_OFF',
signatureTimestamp: this.#session.sts,
autoCaptionsDefaultOn: false,
html5Preference: 'HTML5_PREF_WANTS',
lactMilliseconds: '-1'
}
},
context: this.#session.context,
videoId: id
};
cpn && (data.cpn = cpn);
const response = await this.#request.post('/player', JSON.stringify(data));
return response.data;
}
const response = await ({
'YOUTUBE': async () => {
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${encodeURIComponent(input)}`,
Constants.DEFAULT_HEADERS(session)).catch((error) => error);
return {
success: !(response instanceof Error),
status_code: response.status,
data: response?.data
};
},
'YTMUSIC': async () => {
const response = await music(session, 'get_search_suggestions', { input });
return response;
}
}[client])();
/**
* Covers search suggestion endpoints.
*
* @param {string} client
* @param {string} input
*
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
*/
async getSearchSuggestions(client, input) {
if (!['YOUTUBE', 'YTMUSIC'].includes(client))
throw new Utils.InnertubeError('Invalid client', client);
const response = await ({
YOUTUBE: () => this.#request({
baseURL: Constants.URLS.YT_SUGGESTIONS + `search?client=firefox&ds=yt&q=${encodeURIComponent(input)}`,
}),
YTMUSIC: () => this.music('get_search_suggestions', {
input
})
}[client])();
return response;
}
return response;
#needsLogin(id) {
return [
'FElibrary', 'FEhistory', 'FEsubscriptions',
'SPaccount_notifications', 'SPaccount_privacy'
].includes(id);
}
}
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, next, getSearchSuggestions };
module.exports = Actions;
'use strict';
const Actions = require('./Actions');
const EventEmitter = require('events');

@@ -30,3 +29,3 @@

const livechat = await Actions.livechat(this.session, 'live_chat/get_live_chat', { ctoken: this.ctoken });
const livechat = await this.session.actions.livechat('live_chat/get_live_chat', { ctoken: this.ctoken });
if (!livechat.success) {

@@ -50,5 +49,5 @@ this.emit('error', { message: `Failed polling livechat: ${livechat.message}. Retrying...` });

const data = { video_id: this.video_id };
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
if (this.metadata_ctoken) data.ctoken = this.metadata_ctoken;
const updated_metadata = await Actions.livechat(this.session, 'updated_metadata', data);
const updated_metadata = await this.session.actions.livechat('updated_metadata', data);
if (!updated_metadata.success) {

@@ -94,7 +93,7 @@ this.emit('error', { message: `Failed polling livechat metadata: ${livechat.message}.` });

async sendMessage(text) {
const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
const message = await this.session.actions.livechat('live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
if (!message.success) return message;
const deleteMessage = async () => {
const menu = await Actions.livechat(this.session, 'live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
const menu = await this.session.actions.livechat('live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
if (!menu.success) return menu;

@@ -104,3 +103,3 @@

const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
const cmd = await this.session.actions.livechat('live_chat/moderate', { params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
if (!cmd.success) return cmd;

@@ -107,0 +106,0 @@

@@ -200,3 +200,3 @@ 'use strict';

const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error);
const response = await Axios.get(script_url).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`);

@@ -203,0 +203,0 @@

@@ -9,32 +9,69 @@ 'use strict';

class Player {
constructor(session) {
this.session = session;
this.player_name = Utils.getStringBetweenStrings(this.session.player_url, '/player/', '/');
this.tmp_cache_dir = __dirname.slice(0, -8) + 'cache';
#player_id;
#player_url;
#player_path;
#ntoken_decipher_sc;
#signature_decipher_sc;
#signature_timestamp;
#cache_dir;
constructor(id) {
this.#player_id = id;
this.#cache_dir = __dirname.slice(0, -8) + 'cache';
this.#player_url = Constants.URLS.YT_BASE + '/s/player/' + this.#player_id + '/player_ias.vflset/en_US/base.js';
this.#player_path = `${this.#cache_dir}/${this.#player_id}.js`;
}
async init() {
if (Fs.existsSync(`${this.tmp_cache_dir}/${this.player_name}.js`)) {
const player_data = Fs.readFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`).toString();
this.sig_decipher_sc = this.#getSigDecipherCode(player_data);
this.ntoken_sc = this.#getNEncoder(player_data);
if (this.isCached()) {
const player_data = Fs.readFileSync(this.#player_path).toString();
this.#signature_timestamp = this.#extractSigTimestamp(player_data);
this.#signature_decipher_sc = this.#extractSigDecipherSc(player_data);
this.#ntoken_decipher_sc = this.#extractNTokenSc(player_data);
} else {
const response = await Axios.get(`${Constants.URLS.YT_BASE}${this.session.player_url}`, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
if (response instanceof Error) throw new Error('Could not download player script: ' + response.message);
const response = await Axios.get(this.#player_url, { headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
if (response instanceof Error) throw new Utils.InnertubeError('Could not download js player', { player_id });
this.#signature_timestamp = this.#extractSigTimestamp(response.data);
this.#signature_decipher_sc = this.#extractSigDecipherSc(response.data);
this.#ntoken_decipher_sc = this.#extractNTokenSc(response.data);
try {
// Deletes old players
Fs.existsSync(this.tmp_cache_dir) && Fs.rmSync(this.tmp_cache_dir, { recursive: true });
// Caches the current player so we don't have to download it all the time.
Fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
Fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data);
// Delete the old player
Fs.existsSync(this.#cache_dir) &&
Fs.rmSync(this.#cache_dir, { recursive: true });
// Cache the current player
Fs.mkdirSync(this.#cache_dir, { recursive: true });
Fs.writeFileSync(this.#player_path, response.data);
} catch (err) {}
this.sig_decipher_sc = this.#getSigDecipherCode(response.data);
this.ntoken_sc = this.#getNEncoder(response.data);
}
return this;
}
#getSigDecipherCode(data) {
get url() {
return this.#player_url;
}
get sts() {
return this.#signature_timestamp;
}
get ntoken_decipher() {
return this.#ntoken_decipher_sc;
}
get signature_decipher() {
return this.#signature_decipher_sc;
}
#extractSigTimestamp(data) {
return parseInt(Utils.getStringBetweenStrings(data, 'signatureTimestamp:', ','));
}
#extractSigDecipherSc(data) {
const sig_alg_sc = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};');

@@ -45,7 +82,11 @@ const sig_data = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');

#getNEncoder(data) {
#extractNTokenSc(data) {
return `var b=a.split("")${Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`;
}
isCached() {
return Fs.existsSync(this.#player_path);
}
}
module.exports = Player;

@@ -14,3 +14,3 @@ 'use strict';

* Solves throttling challange by transforming the n token.
* @returns {string} transformed token.
* @returns {string}
*/

@@ -56,3 +56,6 @@ transform() {

} catch (err) {
console.error(`Could not transform n-token (${this.n}), download may be throttled:`, err.message);
console.error(new Utils.ParsingError('Could not transform n-token, download may be throttled.', {
original_token: this.n,
stack: err.stack
}));
return this.n;

@@ -64,3 +67,3 @@ }

#getFunc(el) {
return el.match(Constants.FUNCS_REGEX);
return el.match(Constants.NTOKEN_REGEX.FUNCTIONS);
}

@@ -70,3 +73,3 @@

* Takes the n-transform data, refines it, and then returns a readable json array.
* @returns {object}
* @returns {Array}
*/

@@ -73,0 +76,0 @@ #getTransformationData() {

@@ -10,11 +10,13 @@ 'use strict';

const OAuth = require('./core/OAuth');
const Player = require('./core/Player');
const Actions = require('./core/Actions');
const Livechat = require('./core/Livechat');
const SessionBuilder = require('./core/SessionBuilder');
const Utils = require('./utils/Utils');
const Request = require('./utils/Request');
const Constants = require('./utils/Constants');
const Proto = require('./proto');
const NToken = require('./deciphers/NToken');
const SigDecipher = require('./deciphers/Sig');
const Signature = require('./deciphers/Signature');

@@ -24,2 +26,3 @@ class Innertube {

#player;
#actions;
#retry_count;

@@ -32,76 +35,55 @@

* ```
* @param {string} [cookie]
*
* @param {object} [config]
* @param {string} [config.gl]
* @param {string} [config.cookie]
* @param {boolean} [config.debug]
*
* @returns {Innertube}
* @constructor
*/
constructor(cookie) {
this.cookie = cookie || '';
constructor(config) {
this.config = config || {};
this.#retry_count = 0;
return this.#init();
}
async #init() {
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve Innertube session', { status_code: response.status || 0 });
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') || ''}}`);
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.version = data.INNERTUBE_API_VERSION;
this.context = data.INNERTUBE_CONTEXT;
this.player_url = data.PLAYER_JS_URL;
this.logged_in = data.LOGGED_IN;
this.sts = data.STS;
this.context.client.hl = 'en';
this.context.client.gl = 'US';
/**
* @event Innertube#auth - Fired when signing in to an account.
* @event Innertube#update-credentials - Fired when the access token is no longer valid.
* @type {EventEmitter}
*/
this.ev = new EventEmitter();
this.#oauth = new OAuth(this.ev);
this.#player = new Player(this);
await this.#player.init();
if (this.logged_in && this.cookie.length) {
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
// Axios instances
this.YTRequester = Axios.create({
baseURL: Constants.URLS.YT_BASE_API + this.version,
timeout: 15000,
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false }),
params: { key: this.key }
});
this.YTMRequester = Axios.create({
baseURL: Constants.URLS.YT_MUSIC_BASE_API + this.version,
timeout: 15000,
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true }),
params: { key: this.key }
});
this.#initMethods();
} else {
this.#retry_count += 1;
if (this.#retry_count >= 10)
throw new Utils.ParsingError('No InnerTubeContext shell provided in ytconfig.', {
data_snippet: response.data.slice(0, 300),
status_code: response.status || 0
});
return this.#init();
const session = await new SessionBuilder(this.config).build();
this.key = session.key;
this.version = session.api_version;
this.context = session.context;
this.logged_in = false;
this.player_url = session.player.url;
this.sts = session.player.sts;
this.#player = session.player;
/**
* @fires Innertube#auth - fired when signing in to an account.
* @fires Innertube#update-credentials - fired when the access token is no longer valid.
* @type {EventEmitter}
*/
this.ev = new EventEmitter();
this.#oauth = new OAuth(this.ev);
if (this.config.cookie) {
this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
this.request = new Request(this);
this.actions = new Actions(this);
this.#initMethods();
return this;
}
#initMethods() {
this.account = {
info: () => this.getAccountInfo(),
getTimeWatched: () => { /* TODO: Implement this */ },
settings: {

@@ -113,5 +95,5 @@ notifications: {

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setSubscriptions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'account_notifications', new_value),
setSubscriptions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', new_value),

@@ -122,5 +104,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setRecommendedVideos: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'account_notifications', new_value),
setRecommendedVideos: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', new_value),

@@ -131,5 +113,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setChannelActivity: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'account_notifications', new_value),
setChannelActivity: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', new_value),

@@ -140,5 +122,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setCommentReplies: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'account_notifications', new_value),
setCommentReplies: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', new_value),

@@ -149,5 +131,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setMentions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'account_notifications', new_value),
setMentions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', new_value),

@@ -158,5 +140,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setSharedContent: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'account_notifications', new_value)
setSharedContent: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', new_value)
},

@@ -168,5 +150,5 @@ privacy: {

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'account_privacy', new_value),
setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', new_value),

@@ -177,5 +159,5 @@ /**

* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setSavedPlaylistsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'account_privacy', new_value)
setSavedPlaylistsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', new_value)
}

@@ -190,5 +172,5 @@ }

* @param {string} video_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
like: (video_id) => Actions.engage(this, 'like/like', { video_id }),
like: (video_id) => this.actions.engage('like/like', { video_id }),

@@ -199,5 +181,5 @@ /**

* @param {string} video_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
dislike: (video_id) => Actions.engage(this, 'like/dislike', { video_id }),
dislike: (video_id) => this.actions.engage('like/dislike', { video_id }),

@@ -208,5 +190,5 @@ /**

* @param {string} video_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
removeLike: (video_id) => Actions.engage(this, 'like/removelike', { video_id }),
removeLike: (video_id) => this.actions.engage('like/removelike', { video_id }),

@@ -218,13 +200,42 @@ /**

* @param {string} text
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
comment: (video_id, text) => Actions.engage(this, 'comment/create_comment', { video_id, text }),
comment: (video_id, text) => this.actions.engage('comment/create_comment', { video_id, text }),
/**
* Translates a given text using YouTube's comment translate feature.
*
* @param {string} text
* @param {string} target_language
* @param {object} [args] - optional arguments
* @param {string} [args.video_id]
* @param {string} [args.comment_id]
*
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
translate: async (text, target_language, args = {}) => {
const response = await this.actions.engage('comment/perform_comment_action', {
text,
video_id: args.video_id,
comment_id: args.comment_id,
target_language: target_language,
comment_action: 'translate'
});
const translated_content = Utils.findNode(response.data, 'frameworkUpdates', 'content', 7, false);
return {
success: response.success,
status_code: response.status_code,
translated_content: translated_content.content
}
},
/**
* Subscribes to a given channel.
*
* @param {string} channel_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
subscribe: (channel_id) => Actions.engage(this, 'subscription/subscribe', { channel_id }),
subscribe: (channel_id) => this.actions.engage('subscription/subscribe', { channel_id }),

@@ -235,5 +246,5 @@ /**

* @param {string} channel_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
unsubscribe: (channel_id) => Actions.engage(this, 'subscription/unsubscribe', { channel_id }),
unsubscribe: (channel_id) => this.actions.engage('subscription/unsubscribe', { channel_id }),

@@ -246,5 +257,5 @@ /**

* @param {string} type PERSONALIZED | ALL | NONE
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
changeNotificationPreferences: (channel_id, type) => Actions.notifications(this, 'modify_channel_preference', { channel_id, pref: type || 'NONE' }),
setNotificationPreferences: (channel_id, type) => this.actions.notifications('modify_channel_preference', { channel_id, pref: type || 'NONE' }),
};

@@ -257,7 +268,17 @@

* @param {string} title
* @param {string} video_id - Note that a video must be supplied, empty playlists cannot be created.
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @param {string} video_ids
*
* @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>}
*/
create: (title, video_id) => Actions.engage(this, 'playlist/create', { title, video_id }),
create: async (title, video_ids) => {
const response = await this.actions.playlist('playlist/create', { title, ids: video_ids });
if (!response.success) return response;
return {
success: true,
status_code: response.status_code,
playlist_id: response.data.playlistId
}
},
/**

@@ -267,13 +288,68 @@ * Deletes a given playlist.

* @param {string} playlist_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>}
*/
delete: (playlist_id) => Actions.engage(this, 'playlist/delete', { playlist_id }),
delete: async (playlist_id) => {
const response = await this.actions.playlist('playlist/delete', { playlist_id });
if (!response.success) return response;
return {
success: true,
status_code: response.status_code,
playlist_id
}
},
/**
* Adds videos to a given playlist.
* Adds an array of videos to a given playlist.
*
* @param {string} playlist_id
* @param {Array.<string>} video_ids
* @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>}
*/
addVideos: (playlist_id, video_ids) => Actions.engage(this, 'browse/edit_playlist', { action: 'ACTION_ADD_VIDEO', playlist_id, video_ids })
addVideos: async (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,
status_code: response.status_code,
playlist_id
}
},
/**
* Removes videos from a given playlist.
*
* @param {string} playlist_id
* @param {Array.<string>} video_ids
* @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>}
*/
removeVideos: async (playlist_id, video_ids) => {
const plinfo = await this.actions.browse(`VL${playlist_id}`);
const list = Utils.findNode(plinfo.data, 'contents', 'contents', 13, false);
if (!list.isEditable) throw new Utils.InnertubeError('This playlist cannot be edited.', playlist_id);
const videos = list.contents.filter((item) => video_ids.includes(item.playlistVideoRenderer.videoId));
const set_video_ids = videos.map((video) => video.playlistVideoRenderer.setVideoId);
const response = await this.actions.playlist('browse/edit_playlist', {
action: 'ACTION_REMOVE_VIDEO',
playlist_id,
ids: set_video_ids
});
if (!response.success) return response;
return {
success: true,
status_code: response.status_code,
playlist_id
}
}
};

@@ -288,8 +364,8 @@ }

* @param {string} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
async #setSetting(setting_id, type, new_value) {
const response = await Actions.browse(this, type);
const response = await this.actions.browse(type);
if (!response.success) return response;
const contents = ({

@@ -303,3 +379,3 @@ account_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options,

const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId;
const set_setting = await Actions.account(this, '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 == 'account_privacy' ? !new_value : new_value, setting_item_id });

@@ -330,3 +406,3 @@ return {

}
this.ev.on('auth', (data) => {

@@ -340,3 +416,3 @@ if (data.status === 'SUCCESS') {

}
#updateCredentials() {

@@ -346,14 +422,6 @@ this.access_token = this.#oauth.getAccessToken();

this.logged_in = true;
// API key is not needed if logged in via OAuth
delete this.YTRequester.defaults.params.key;
delete this.YTMRequester.defaults.params.key;
}
// Update default headers
this.YTRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false });
this.YTMRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true });
}
/**
* Signs out of your account.
* Signs out of an account.
* @returns {Promise.<{ success: boolean; status_code: number }>}

@@ -369,7 +437,7 @@ */

/**
* Returns information about the account being used.
* @returns {Promise<{ name: string; photo: Array<object>; country: string; language: string; }>}
* Retrieves account details.
* @returns {Promise.<{ name: string; photo: Array<object>; country: string; language: string; }>}
*/
async getAccountInfo() {
const response = await Actions.account(this, 'account/account_menu');
const response = await this.actions.account('account/account_menu');
if (!response.success) throw new Utils.InnertubeError('Could not get account info', response);

@@ -390,8 +458,9 @@

*
* @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.duration - Filter video results by duration, can be: any | short | long
* @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.duration - filter video results by duration, can be: any | short | long
*
* @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } |

@@ -401,7 +470,7 @@ * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>}

async search(query, options = { client: 'YOUTUBE', period: 'any', order: 'relevance', duration: 'any' }) {
const response = await Actions.search(this, options.client, { query, options });
if (!response.success) throw new Utils.InnertubeError('Could not search on YouTube', response);
const response = await this.actions.search({ query, options, is_ytm: options.client == 'YTMUSIC' });
const results = new Parser(this, response.data, {
query, client: options.client,
query,
client: options.client,
data_type: 'SEARCH'

@@ -414,11 +483,12 @@ }).parse();

/**
* Gets search suggestions.
* Retrieves search suggestions.
*
* @param {string} input - The search query.
* @param {object} [options] - Search options.
* @param {string} [options.client='YOUTUBE'] - Client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`.
* @param {string} input - the search query.
* @param {object} [options] - search options.
* @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`.
*
* @returns {Promise.<[{ text: string; bold_text: string }]>}
*/
async getSearchSuggestions(input, options = { client: 'YOUTUBE' }) {
const response = await Actions.getSearchSuggestions(this, options.client, input);
const response = await this.actions.getSearchSuggestions(options.client, input);
if (!response.success) throw new Utils.InnertubeError('Could not get search suggestions', response);

@@ -428,6 +498,7 @@ if (options.client === 'YTMUSIC' && !response.data.contents) return [];

const suggestions = new Parser(this, response.data, {
input, client: options.client,
input,
client: options.client,
data_type: 'SEARCH_SUGGESTIONS'
}).parse();
return suggestions;

@@ -437,5 +508,5 @@ }

/**
* Gets video info.
* Retrieves video info.
*
* @param {string} video_id - The id of the video.
* @param {string} video_id - video id
* @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>}

@@ -446,22 +517,25 @@ */

const response = await Actions.getVideoInfo(this, { id: video_id });
const continuation = await Actions.next(this, { video_id });
const response = await this.actions.getVideoInfo(video_id);
const continuation = await this.actions.next({ video_id });
continuation.success && (response.continuation = continuation.data);
const details = new Parser(this, response, {
client: 'YOUTUBE',
client: 'YOUTUBE',
data_type: 'VIDEO_INFO'
}).parse();
// Functions
details.like = () => Actions.engage(this, 'like/like', { video_id });
details.dislike = () => Actions.engage(this, 'like/dislike', { video_id });
details.removeLike = () => Actions.engage(this, 'like/removelike', { video_id });
details.subscribe = () => Actions.engage(this, 'subscription/subscribe', { channel_id: details.metadata.channel_id });
details.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { channel_id: details.metadata.channel_id });
details.comment = (text) => Actions.engage(this, 'comment/create_comment', { video_id, text });
details.getComments = () => this.getComments(video_id, { channel_id: details.metadata.channel_id });
details.getLivechat = () => new Livechat(this, continuation.data.contents?.twoColumnWatchNextResults?.conversationBar?.liveChatRenderer?.continuations?.find((continuation) => continuation.reloadContinuationData).reloadContinuationData.continuation, details.metadata.channel_id, video_id);
details.changeNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
const livechat_ctoken = continuation.data.contents?.twoColumnWatchNextResults
?.conversationBar?.liveChatRenderer?.continuations?.find((continuation) => continuation.reloadContinuationData)
.reloadContinuationData.continuation;
details.like = () => this.actions.engage('like/like', { video_id });
details.dislike = () => this.actions.engage('like/dislike', { video_id });
details.removeLike = () => this.actions.engage('like/removelike', { video_id });
details.subscribe = () => this.actions.engage('subscription/subscribe', { channel_id: details.metadata.channel_id });
details.unsubscribe = () => this.actions.engage('subscription/unsubscribe', { channel_id: details.metadata.channel_id });
details.comment = (text) => this.actions.engage('comment/create_comment', { video_id, text });
details.getComments = (sort_by) => this.getComments(video_id, sort_by);
details.getLivechat = () => new Livechat(this, livechat_ctoken, details.metadata.channel_id, video_id);
details.setNotificationPreferences = (type) => this.actions.notifications('modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
return details;

@@ -471,165 +545,56 @@ }

/**
* Gets info about a given channel. (WIP)
*
* @param {string} id - The id of the channel.
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
* Retrieves comments for a video.
*
* @param {string} video_id - video id
* @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`.
* @return {Promise.<{ page_count: number; comment_count: number; items: []; }>}
*/
async getChannel(id) {
const response = await Actions.browse(this, 'channel', { browse_id: 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'
}).parse();
return channel_info;
}
async getComments(video_id, sort_by) {
const payload = Proto.encodeCommentsSectionParams(video_id, {
sort_by: sort_by || 'TOP_COMMENTS'
});
/**
* Retrieves the lyrics for a given song if available.
*
* @param {string} video_id
* @returns {Promise.<string>} Song lyrics
*/
async getLyrics(video_id) {
const continuation = await Actions.next(this, { video_id: video_id, ytmusic: true });
if (!continuation.success) throw new Utils.InnertubeError('Could not retrieve lyrics', continuation);
const response = await this.actions.next({ ctoken: payload });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve comments', response);
const lyrics_tab = continuation.data.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer
.watchNextTabbedResultsRenderer.tabs.find((obj) => obj.tabRenderer.title == 'Lyrics');
const comments = new Parser(this, response.data, {
video_id,
client: 'YOUTUBE',
data_type: 'COMMENTS'
}).parse();
const response = await Actions.browse(this, 'lyrics', { ytmusic: true, browse_id: lyrics_tab.tabRenderer.endpoint.browseEndpoint.browseId });
if (!response.success || !response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id });
const lyrics = response.data.contents.sectionListRenderer.contents[0].musicDescriptionShelfRenderer.description.runs[0].text;
return lyrics;
return comments;
}
/**
* Parses a given playlist.
* Retrieves contents for a given channel. (WIP)
*
* @param {string} playlist_id - The id of the playlist.
* @param {object} options - { client: YOUTUBE | YTMUSIC }
* @param {string} options.client - Client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE`
* @returns {Promise.<
* { title: string; description: string; total_items: string; last_updated: string; views: string; items: [] } |
* { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>}
* @param {string} id - channel id
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
*/
async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) {
const response = await Actions.browse(this, options.client == 'YTMUSIC' ? 'music_playlist' : 'playlist', { ytmusic: options.client == 'YTMUSIC', browse_id: `VL${playlist_id}` });
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'
async getChannel(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'
}).parse();
return playlist;
}
/**
* Gets the comments section of a video.
*
* @param {string} video_id - The id of the video.
* @param {string} [data] - Video data and continuation token (optional).
* @return {Promise.<[{ comments: []; comment_count?: string }]>
*/
async getComments(video_id, data = {}) {
let comment_section_token;
//TODO: Refactor this and move it to the parser
if (!data.token) {
const continuation = await Actions.next(this, { video_id });
if (!continuation.success) throw new Utils.InnertubeError('Could not fetch comments section', continuation);
const contents = Utils.findNode(continuation.data, 'contents', 'comments-section', 5);
const item_section_renderer = contents.find((item) => item.itemSectionRenderer).itemSectionRenderer;
comment_section_token = item_section_renderer?.contents[0]?.continuationItemRenderer?.continuationEndpoint.continuationCommand.token;
const secondary_info_renderer = contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer;
data.channel_id = secondary_info_renderer.owner.videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId;
}
const response = await Actions.next(this, { continuation_token: comment_section_token || data.token });
if (!response.success) throw new Utils.InnertubeError('Could not fetch comments section', response);
const comments_section = { comments: [] };
!data.token && (comments_section.comment_count = response.data?.onResponseReceivedEndpoints[0]?.reloadContinuationItemsCommand?.continuationItems[0]?.commentsHeaderRenderer?.countText.runs[0]?.text || 'N/A');
let continuation_token;
!data.token &&
(continuation_token = response.data?.onResponseReceivedEndpoints[1]?.reloadContinuationItemsCommand?.continuationItems
?.find((item) => item.continuationItemRenderer)?.continuationItemRenderer?.continuationEndpoint?.continuationCommand.token) ||
((continuation_token = response.data?.onResponseReceivedEndpoints[0]?.appendContinuationItemsAction?.continuationItems
?.find((item) => item.continuationItemRenderer)?.continuationItemRenderer?.continuationEndpoint?.continuationCommand.token) ||
(continuation_token = response.data?.onResponseReceivedEndpoints[0]?.appendContinuationItemsAction?.continuationItems
?.find((item) => item.continuationItemRenderer)?.continuationItemRenderer?.button.buttonRenderer.command.continuationCommand.token));
continuation_token && (comments_section.getContinuation =
() => this.getComments(video_id, {
token: continuation_token,
channel_id: data.channel_id
}));
let contents;
!data.token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems) ||
(contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
contents.forEach((content) => {
const thread = content?.commentThreadRenderer?.comment.commentRenderer || content?.commentRenderer;
if (!thread) return;
// TODO: Reverse engineer this token so we can generate it manually (it's just protobuf).
const replies_token = content?.commentThreadRenderer?.replies?.commentRepliesRenderer?.contents
?.find((content) => content.continuationItemRenderer.continuationEndpoint)
?.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
const like_btn = thread?.actionButtons?.commentActionButtonsRenderer.likeButton;
const dislike_btn = thread?.actionButtons?.commentActionButtonsRenderer.dislikeButton;
const comment = {
text: thread.contentText.runs.map((t) => t.text).join(' '),
author: {
name: thread.authorText.simpleText,
thumbnail: thread.authorThumbnail.thumbnails,
channel_id: thread.authorEndpoint.browseEndpoint.browseId
},
metadata: {
published: thread.publishedTimeText.runs[0].text,
is_liked: like_btn?.toggleButtonRenderer.isToggled,
is_disliked: dislike_btn?.toggleButtonRenderer.isToggled,
is_pinned: thread.pinnedCommentBadge && true || false,
is_channel_owner: thread.authorIsChannelOwner,
like_count: thread?.voteCount?.simpleText || '0',
reply_count: thread.replyCount || 0,
id: thread.commentId,
},
like: () => Actions.engage(this, 'comment/perform_comment_action', { comment_action: 'like', comment_id: thread.commentId, video_id, channel_id: data.channel_id }),
dislike: () => Actions.engage(this, 'comment/perform_comment_action', { comment_action: 'dislike', comment_id: thread.commentId, video_id, channel_id: data.channel_id }),
reply: (text) => Actions.engage(this, 'comment/create_comment_reply', { text, comment_id: thread.commentId, video_id }),
getReplies: () => this.getComments(video_id, { token: replies_token, channel_id: data.channel_id })
};
!replies_token && (delete comment.getReplies);
comments_section.comments.push(comment);
});
return comments_section;
return channel_info;
}
/**
* Returns your watch history.
* Retrieves watch history.
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
*/
async getHistory() {
const response = await Actions.browse(this, 'history');
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',
client: 'YOUTUBE',
data_type: 'HISTORY'
}).parse();
return history;

@@ -639,21 +604,28 @@ }

/**
* Returns YouTube's home feed (aka recommendations).
* Retrieves YouTube's home feed (aka recommendations).
* @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>}
*/
async getHomeFeed() {
const response = await Actions.browse(this, 'home_feed');
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',
client: 'YOUTUBE',
data_type: 'HOMEFEED'
}).parse();
return homefeed;
}
/**
* Retrieves trending content.
*
* @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] };
* music: { getVideos: Promise.<Array>; }; gaming: { getVideos: Promise.<Array>; };
* gaming: { getVideos: Promise.<Array>; }; }>}
*/
async getTrending() {
const response = await Actions.browse(this, 'trending');
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, {

@@ -663,16 +635,31 @@ client: 'YOUTUBE',

}).parse();
return trending;
}
/**
* WIP
*/
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'
}).parse();
return library;
}
/**
* Returns your subscription feed.
* Retrieves subscriptions feed.
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
*/
async getSubscriptionsFeed() {
const response = await Actions.browse(this, 'subscriptions_feed');
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',
client: 'YOUTUBE',
data_type: 'SUBSFEED'

@@ -689,10 +676,10 @@ }).parse();

async getNotifications() {
const response = await Actions.notifications(this, 'get_notification_menu');
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',
client: 'YOUTUBE',
data_type: 'NOTIFICATIONS'
}).parse();
return notifications;

@@ -702,7 +689,7 @@ }

/**
* Returns unseen notifications count.
* @returns {Promise.<number>} unseen notifications count.
* Retrieves unseen notifications count.
* @returns {Promise.<number>}
*/
async getUnseenNotificationsCount() {
const response = await Actions.notifications(this, 'get_unseen_count');
const response = await this.actions.notifications('get_unseen_count');
if (!response.success) throw new Utils.InnertubeError('Could not get unseen notifications count', response);

@@ -713,2 +700,43 @@ return response.data.unseenCount;

/**
* Retrieves lyrics for a given song if available.
*
* @param {string} video_id
* @returns {Promise.<string>}
*/
async getLyrics(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 });
const lyrics = Utils.findNode(response.data, 'contents', 'runs', 6, false);
return lyrics.runs[0].text;
}
/**
* Retrieves a given playlist.
*
* @param {string} playlist_id - playlist id.
* @param {object} options - { client: YOUTUBE | YTMUSIC }
* @param {string} options.client - client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE`
* @returns {Promise.<
* { title: string; description: string; total_items: string; last_updated: string; views: string; items: [] } |
* { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>}
*/
async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) {
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'
}).parse();
return playlist;
}
/**
* Internal method to process and filter formats.

@@ -731,3 +759,3 @@ *

if (format.signatureCipher || format.cipher) {
format.url = new SigDecipher(format.url, this.#player).decipher();
format.url = new Signature(format.url, this.#player.signature_decipher).decipher();
}

@@ -740,3 +768,3 @@

if (url_components.searchParams.get('n')) {
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc, url_components.searchParams.get('n')).transform());
url_components.searchParams.set('n', new NToken(this.#player.ntoken_decipher, url_components.searchParams.get('n')).transform());
}

@@ -752,5 +780,2 @@

formats.hls_manifest_url = video_data.streamingData.hlsManifestUrl || undefined;
formats.dash_manifest_url = video_data.streamingData.dashManifestUrl || undefined;
let format;

@@ -766,17 +791,13 @@ let bitrates;

if (options.type != 'videoandaudio') {
let streams;
let streams;
options.type != 'audio' &&
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4')));
options.type != 'audio' &&
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4')));
streams == undefined || streams.length == 0 &&
(streams = filtered_formats.filter((format) => format.quality == 'medium'));
!streams || !streams.length &&
(streams = filtered_formats.filter((format) => format.quality == 'medium'));
bitrates = streams.map((format) => format.bitrate);
format = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];
} else {
format = filtered_formats[0];
}
bitrates = streams.map((format) => format.bitrate);
format = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];

@@ -790,7 +811,8 @@ return { selected_format: format, formats };

*
* @param {string} id - The id of the video.
* @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
* @param {string} options.format - File format
* @param {string} 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
* @param {string} options.format - file format
*
* @returns {Promise.<{ selected_format: {}; formats: [] }>}

@@ -803,6 +825,8 @@ */

const data = await Actions.getVideoInfo(this, { id });
const data = await this.actions.getVideoInfo(id);
const streaming_data = this.#chooseFormat(options, data);
if (!streaming_data.selected_format) throw new Utils.NoStreamingDataError('Could not find any suitable format.', { id, options });
if (!streaming_data.selected_format)
throw new Utils.NoStreamingDataError('Could not find any suitable format.', { id, options });
return streaming_data;

@@ -814,8 +838,9 @@ }

*
* @param {string} id - The id of the video.
* @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
* @param {string} options.format - File format
* @return {ReadableStream}
* @param {string} 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
* @param {string} [options.format] - file format
*
* @return {Stream.PassThrough}
*/

@@ -831,5 +856,7 @@ download(id, options = {}) {

let cancelled = false;
const cpn = Utils.generateRandomString(16);
const stream = new Stream.PassThrough();
Actions.getVideoInfo(this, { id }).then(async (video_data) => {
this.actions.getVideoInfo(id, cpn).then(async (video_data) => {
if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED')

@@ -849,3 +876,3 @@ return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status });

if (options.type == 'videoandaudio' && !options.range) {
const response = await Axios.get(format.url, {
const response = await Axios.get(`${format.url}&cpn=${cpn}`, {
responseType: 'stream',

@@ -903,4 +930,4 @@ cancelToken: new CancelToken(function executor(c) { cancel = c; }),

options.range && (format.contentLength = options.range.end);
const response = await Axios.get(`${format.url}&range=${chunk_start}-${chunk_end || ''}`, {
const response = await Axios.get(`${format.url}&cpn=${cpn}&range=${chunk_start}-${chunk_end || ''}`, {
responseType: 'stream',

@@ -965,2 +992,2 @@ cancelToken: new CancelToken(function executor(c) { cancel = c; }),

module.exports = Innertube;
module.exports = Innertube;
'use strict';
const Utils = require('../utils/Utils');
const Actions = require('../core/Actions');
const Constants = require('../utils/Constants');
const YTDataItems = require('./youtube');
const YTMusicDataItems = require('./ytmusic');
const Proto = require('../proto');

@@ -30,4 +30,6 @@ class Parser {

HOMEFEED: () => this.#processHomeFeed(),
LIBRARY: () => this.#processLibrary(), //WIP
TRENDING: () => this.#processTrending(),
HISTORY: () => this.#processHistory(),
COMMENTS: () => this.#processComments(),
VIDEO_INFO: () => this.#processVideoInfo(),

@@ -70,3 +72,3 @@ NOTIFICATIONS: () => this.#processNotifications(),

const response = await Actions.search(this.session, 'YOUTUBE', { ctoken });
const response = await this.session.actions.search({ ctoken, is_ytm: false });
if (!response.success) throw new Utils.InnertubeError('Could not get continuation', response);

@@ -87,3 +89,3 @@

const contents = Utils.findNode(tabs, '0', 'contents', 5);
const did_you_mean_item = contents.find((content) => content.itemSectionRenderer);

@@ -107,3 +109,3 @@ const did_you_mean_renderer = did_you_mean_item?.itemSectionRenderer.contents[0].didYouMeanRenderer;

const section_items = ({
['Top result']: () => YTMusicDataItems.TopResultItem.parse(section.contents), // console.log(JSON.stringify(section.contents, null, 4)),
['Top result']: () => YTMusicDataItems.TopResultItem.parse(section.contents),
['Songs']: () => YTMusicDataItems.SongResultItem.parse(section.contents),

@@ -135,8 +137,8 @@ ['Videos']: () => YTMusicDataItems.VideoResultItem.parse(section.contents),

const details = this.data.sidebar.playlistSidebarRenderer.items[0];
const metadata = {
title: this.data.metadata.playlistMetadataRenderer.title,
description: details.playlistSidebarPrimaryInfoRenderer.description.simpleText || 'N/A',
total_items: details.playlistSidebarPrimaryInfoRenderer.stats[0].runs[0].text,
last_updated: details.playlistSidebarPrimaryInfoRenderer.stats[2].runs[1].text,
description: details.playlistSidebarPrimaryInfoRenderer?.description?.simpleText || 'N/A',
total_items: details.playlistSidebarPrimaryInfoRenderer.stats[0].runs[0]?.text || 'N/A',
last_updated: details.playlistSidebarPrimaryInfoRenderer.stats[2].runs[1]?.text || 'N/A',
views: details.playlistSidebarPrimaryInfoRenderer.stats[1].simpleText

@@ -276,2 +278,173 @@ }

#processComments() {
if (!this.data.onResponseReceivedEndpoints)
throw new Utils.UnavailableContentError('Comments section not available', this.args);
const header = Utils.findNode(this.data, 'onResponseReceivedEndpoints', 'commentsHeaderRenderer', 5, false);
const comment_count = parseInt(header.commentsHeaderRenderer.countText.runs[0].text.replace(/,/g, ''));
const page_count = parseInt(comment_count / 20);
const parseComments = (data) => {
const items = Utils.findNode(data, 'onResponseReceivedEndpoints', 'commentRenderer', 4, false);
const response = {
page_count,
comment_count,
items: []
};
response.items = items.map((item) => {
const comment = YTDataItems.CommentThread.parseItem(item);
if (comment) {
comment.like = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id }),
comment.dislike = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id }),
comment.reply = (text) => this.session.actions.engage('comment/create_comment_reply', { text, comment_id: comment.metadata.id, video_id: this.args.video_id });
comment.report = async () => {
const payload = Utils.findNode(item, 'commentThreadRenderer', 'params', 10, false);
const form = await this.session.actions.flag('flag/get_form', { params: payload.params });
const action = Utils.findNode(form, 'actions', 'flagAction', 13, false);
const flag = await this.session.actions.flag('flag/flag', { action: action.flagAction });
return flag;
};
comment.getReplies = async () => {
if (comment.metadata.reply_count === 0) throw new Utils.InnertubeError('This comment has no replies', comment);
const payload = Proto.encodeCommentRepliesParams(this.args.video_id, comment.metadata.id);
const next = await this.session.actions.next({ ctoken: payload });
return parseComments(next.data);
};
comment.translate = async (target_language) => {
const response = await this.session.actions.engage('comment/perform_comment_action', {
text: comment.text,
comment_action: 'translate',
comment_id: comment.metadata.id,
video_id: this.args.video_id,
target_language
});
const translated_content = Utils.findNode(response.data, 'frameworkUpdates', 'content', 7, false);
return {
success: response.success,
status_code: response.status_code,
translated_content: translated_content.content
}
}
return comment;
}
}).filter((c) => c);
response.comment = (text) => this.session.actions.engage('comment/create_comment', { video_id: this.args.video_id, text });
response.getContinuation = async () => {
const continuation_item = items.find((item) => item.continuationItemRenderer);
if (!continuation_item) throw new Utils.InnertubeError('You\'ve reached the end');
const is_reply = !!continuation_item.continuationItemRenderer.button;
const payload = Utils.findNode(continuation_item, 'continuationItemRenderer', 'token', is_reply && 5 || 3);
const next = await this.session.actions.next({ ctoken: payload.token });
return parseComments(next.data);
};
return response;
};
return parseComments(this.data);
}
#processHomeFeed() {
const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false)
const parseItems = (contents) => {
const videos = YTDataItems.VideoItem.parse(contents);
const getContinuation = async () => {
const citem = contents.find((item) => item.continuationItemRenderer);
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);
}
return { videos, getContinuation };
}
return parseItems(contents);
}
#processLibrary() { // TODO: Finish this
const profile_data = Utils.findNode(this.data, 'contents', 'profileColumnRenderer', 3);
const stats_data = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnStatsRenderer);
const stats_items = stats_data.profileColumnStatsRenderer.items;
const userinfo = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnUserInfoRenderer);
const stats = {};
stats_items.forEach((item) => {
const label = item.profileColumnStatsEntryRenderer.label.runs.map((run) => run.text).join('');
stats[label.toLowerCase()] = parseInt(item.profileColumnStatsEntryRenderer.value.simpleText);
});
const profile = {
name: userinfo.profileColumnUserInfoRenderer?.title?.simpleText,
thumbnails: userinfo.profileColumnUserInfoRenderer?.thumbnail.thumbnails,
stats
}
const content = Utils.findNode(this.data, 'contents', 'content', 8, false);
// console.info(content[0].itemSectionRenderer.contents[0].shelfRenderer);
return {
profile
}
}
#processSubscriptionFeed() {
const contents = Utils.findNode(this.data, 'contents', 'contents', 9, false);
const subsfeed = { items: [] };
const parseItems = (contents) => {
contents.forEach((section) => {
if (!section.itemSectionRenderer) return;
const section_contents = section.itemSectionRenderer.contents[0];
const section_title = section_contents.shelfRenderer.title.runs[0].text;
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
const items = YTDataItems.GridVideoItem.parse(section_items);
subsfeed.items.push({
date: section_title,
videos: items
});
});
subsfeed.getContinuation = async () => {
const citem = contents.find((item) => item.continuationItemRenderer);
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 = [];
return parseItems(ccontents);
}
return subsfeed;
};
return parseItems(contents);
}
#processChannel() {

@@ -337,3 +510,3 @@ const tabs = this.data.contents.twoColumnBrowseResultsRenderer.tabs;

const response = await Actions.notifications(this.session, 'get_notification_menu', { ctoken });
const response = await this.session.actions.notifications('get_notification_menu', { ctoken });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);

@@ -352,3 +525,2 @@

const tabs = Utils.findNode(this.data, 'contents', 'tabRenderer', 4, false);
const categories = {};

@@ -374,3 +546,3 @@

categories[category_title].getVideos = async () => {
const response = await Actions.browse(this.session, 'trending', { params });
const response = await this.session.actions.browse('FEtrending', { params });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve category videos', response);

@@ -417,3 +589,3 @@

const response = await Actions.browse(this.session, 'continuation', { ctoken });
const response = await this.session.actions.browse(ctoken, { is_ctoken: true });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);

@@ -431,66 +603,4 @@

}
#processHomeFeed() {
const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false)
const parseItems = (contents) => {
const videos = YTDataItems.VideoItem.parse(contents);
const getContinuation = async () => {
const citem = contents.find((item) => item.continuationItemRenderer);
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
const response = await Actions.browse(this.session, 'continuation', { ctoken });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems);
}
return { videos, getContinuation };
}
return parseItems(contents);
}
#processSubscriptionFeed() {
const contents = Utils.findNode(this.data, 'contents', 'contents', 9, false);
const subsfeed = { items: [] };
const parseItems = (contents) => {
contents.forEach((section) => {
if (!section.itemSectionRenderer) return;
const section_contents = section.itemSectionRenderer.contents[0];
const section_title = section_contents.shelfRenderer.title.runs[0].text;
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
const items = YTDataItems.GridVideoItem.parse(section_items);
subsfeed.items.push({
date: section_title,
videos: items
});
});
subsfeed.getContinuation = async () => {
const citem = contents.find((item) => item.continuationItemRenderer);
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
const response = await Actions.browse(this.session, 'continuation', { ctoken });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false);
subsfeed.items = [];
return parseItems(ccontents);
}
return subsfeed;
};
return parseItems(contents);
}
}
module.exports = Parser;
module.exports = Parser;

@@ -12,3 +12,4 @@ 'use strict';

const ShelfRenderer = require('./others/ShelfRenderer');
const CommentThread = require('./others/CommentThread');
module.exports = { VideoResultItem, SearchSuggestionItem, PlaylistItem, NotificationItem, VideoItem, GridVideoItem, GridPlaylistItem, ChannelMetadata, ShelfRenderer };
module.exports = { VideoResultItem, SearchSuggestionItem, PlaylistItem, NotificationItem, VideoItem, GridVideoItem, GridPlaylistItem, ChannelMetadata, ShelfRenderer, CommentThread };

@@ -21,3 +21,3 @@ 'use strict';

name: renderer?.ownerText?.runs[0]?.text,
url: `${Constants.URLS.YT_BASE}${renderer.ownerText.runs[0].navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
url: `${Constants.URLS.YT_BASE}${renderer?.ownerText?.runs[0].navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
},

@@ -24,0 +24,0 @@ metadata: {

@@ -10,4 +10,4 @@ 'use strict';

const list_item = item.musicResponsiveListItemRenderer;
const watch_playlist_endpoint = list_item.overlay.musicItemThumbnailOverlayRenderer
.content.musicPlayButtonRenderer.playNavigationEndpoint.watchPlaylistEndpoint;
const watch_playlist_endpoint = list_item?.overlay?.musicItemThumbnailOverlayRenderer
?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint;

@@ -14,0 +14,0 @@ return {

'use strict';
const Fs = require('fs');
const Proto = require('protons');
const messages = require('./messages');
/**
* Encodes advanced search filters.
*
* @param {string} period - Period in which a video is uploaded: any | hour | day | week | month | year
* @param {string} duration - The duration of a video: any | short | long
* @param {string} order - The order of the search results: relevance | rating | age | views
* @returns {string}
*/
function encodeSearchFilter(period, duration, order) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
class Proto {
/**
* Encodes visitor data.
*
* @param {string} id
* @param {number} timestamp
*
* @returns {string}
*/
static encodeVisitorData(id, timestamp) {
const buf = messages.VisitorData.encode({ id, timestamp });
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes search filters.
*
* @param {string} period
* @param {string} duration
* @param {string} order
*
* @todo implement remaining filters.
*
* @returns {string}
*/
static encodeSearchFilter(period, duration, order) {
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
const durations = { 'any': null, 'short': 1, 'long': 2 };
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
const durations = { 'any': null, 'short': 1, 'long': 2 };
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
const buf = messages.SearchFilter.encode({
number: orders[order],
filter: {
param_0: periods[period],
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
param_2: durations[duration]
}
});
const search_filter_buff = youtube_proto.SearchFilter.encode({
number: orders[order],
filter: {
param_0: periods[period],
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
param_2: durations[duration]
}
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes livechat message parameters.
*
* @param {string} channel_id
* @param {string} video_id
*
* @returns {string}
*/
static encodeMessageParams(channel_id, video_id) {
const buf = messages.LiveMessageParams.encode({
params: { ids: { channel_id, video_id } },
number_0: 1, number_1: 4
});
return encodeURIComponent(Buffer.from(search_filter_buff).toString('base64'));
}
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
}
/**
* Encodes comment section parameters.
*
* @param {string} video_id
* @param {object} options
* @param {string} options.type
* @param {string} options.sort_by
*
* @returns {string}
*/
static encodeCommentsSectionParams(video_id, options = {}) {
const sort_menu = { TOP_COMMENTS: 0, NEWEST_FIRST: 1 };
const buf = messages.GetCommentsSectionParams.encode({
ctx: { video_id },
unk_param: 6,
params: {
opts: {
video_id,
sort_by: sort_menu[options.sort_by || 'TOP_COMMENTS'],
type: options.type || 2
},
target: 'comments-section'
}
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes replies thread parameters.
*
* @param {string} video_id
* @param {string} comment_id
*
* @returns {string}
*/
static encodeCommentRepliesParams(video_id, comment_id) {
const buf = messages.GetCommentsSectionParams.encode({
ctx: { video_id },
unk_param: 6,
params: {
replies_opts: {
video_id, comment_id,
unkopts: { unk_param: 0 },
unk_param_1: 1, unk_param_2: 10,
channel_id: ' ' // Seems like this can be omitted
},
target: `comment-replies-item-${comment_id}`
}
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes comment parameters.
*
* @param {string} video_id
* @returns {string}
*/
static encodeCommentParams(video_id) {
const buf = messages.CreateCommentParams.encode({
video_id, params: { index: 0 },
number: 7
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes comment reply parameters.
*
* @param {string} comment_id
* @param {string} video_id
*
* @return {string}
*/
static encodeCommentReplyParams(comment_id, video_id) {
const buf = messages.CreateCommentReplyParams.encode({
video_id, comment_id,
params: { unk_num: 0 },
unk_num: 7
});
/**
* Encodes livestream message parameters.
*
* @param {string} channel_id - The id of the channel hosting the livestream.
* @param {string} video_id - The id of the livestream.
* @returns {string}
*/
function encodeMessageParams(channel_id, video_id) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes comment action parameters.
*
* @param {string} type
* @param {string} comment_id
* @param {string} video_id
* @param {string} [text]
* @param {string} [target_language]
*
* @returns {string}
*/
static encodeCommentActionParams(type, args = {}) {
const data = {};
data.type = type;
data.video_id = args.video_id || '';
data.comment_id = args.comment_id || '';
data.unk_num = 2;
if (args.hasOwnProperty('text')) {
args.comment_id && (delete data.unk_num);
data.translate_comment_params = {
params: {
comment: {
text: args.text
}
},
comment_id: args.comment_id || '',
target_language: args.target_language
}
}
const buf = messages.PeformCommentActionParams.encode(data);
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes notification preference parameters.
*
* @param {string} channel_id
* @param {number} index
*
* @returns {string}
*/
static encodeNotificationPref(channel_id, index) {
const buf = messages.NotificationPreferences.encode({
channel_id, pref_id: { index },
number_0: 0, number_1: 4
});
const buf = youtube_proto.LiveMessageParams.encode({
params: {
ids: { channel_id, video_id }
},
number_0: 1,
number_1: 4
});
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
}
/**
* Encodes comment parameters.
*
* @param {string} video_id - The id of the video you're commenting on.
* @returns {string}
*/
function encodeCommentParams(video_id) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
const buf = youtube_proto.CreateCommentParams.encode({
video_id,
params: { index: 0 },
number: 7
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes comment reply parameters.
*
* @param {string} comment_id - The id of the comment.
* @param {string} video_id - The id of the video you're commenting on.
* @returns {string}
*/
function encodeCommentReplyParams(comment_id, video_id) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
const buf = youtube_proto.CreateCommentReplyParams.encode({
video_id, comment_id,
params: { unk_num: 0 },
unk_num: 7
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
/**
* Encodes comment action parameters (liking, disliking, reporting a comment etc).
*
* @param {string} type - Type of action.
* @param {string} comment_id - The id of the comment.
* @param {string} video_id - The id of the video you're commenting on.
* @param {string} channel_id - The id of the channel.
* @returns {string}
*/
function encodeCommentActionParams(type, comment_id, video_id, channel_id) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
const buf = youtube_proto.PeformCommentActionParams.encode({
type, comment_id, channel_id, video_id,
unk_num: 2, unk_num_1: 0, unk_num_2: 0,
unk_num_3: "0", unk_num_4: 0,
unk_num_5: 12, unk_num_6: 0,
});
/**
* Encodes sound info parameters.
*
* @param {string} id
* @returns {string}
*/
static encodeSoundInfoParams(id) {
const data = {
sound: {
params: {
ids: {
id_1: id,
id_2: id,
id_3: id
}
}
}
}
return encodeURIComponent(Buffer.from(buf).toString('base64'));
const buf = messages.SoundInfoParams.encode(data);
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
}
/**
* Encodes notification preferences.
*
* @param {string} channel_id - The id of the channel.
* @param {string} index - The index of the preference id.
* @returns {string}
*/
function encodeNotificationPref(channel_id, index) {
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
const buf = youtube_proto.NotificationPreferences.encode({
channel_id,
pref_id: { index },
number_0: 0,
number_1: 4
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
module.exports = { encodeMessageParams, encodeCommentParams, encodeCommentReplyParams, encodeCommentActionParams, encodeNotificationPref, encodeSearchFilter };
module.exports = Proto;

@@ -33,13 +33,11 @@ 'use strict';

},
DEFAULT_HEADERS: (session) => {
return {
headers: {
'Cookie': session.cookie,
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
'Referer': 'https://www.google.com/',
'Accept': 'text/html',
'Accept-Language': 'en-US,en',
'Accept-Encoding': 'gzip'
}
};
CLIENTS: {
YTMUSIC: {
NAME: 'WEB_REMIX',
VERSION: '1.20211213.00.00'
},
ANDROID: {
NAME: 'ANDROID',
VERSION: '17.17.32'
}
},

@@ -54,48 +52,6 @@ STREAM_HEADERS: {

},
INNERTUBE_HEADERS: (info) => {
const origin = info.ytmusic && 'https://music.youtube.com' || 'https://www.youtube.com';
const headers = {
'accept': '*/*',
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
'content-type': 'application/json',
'accept-language': 'en-US,en;q=0.9',
'x-goog-authuser': 0,
'x-goog-visitor-id': info.session.context.client.visitorData || '',
'x-youtube-client-name': 1,
'x-youtube-client-version': info.session.context.client.clientVersion,
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
'x-origin': origin,
'origin': origin
};
const auth_creds = info.session.cookie.length && info.session.auth_apisid || `Bearer ${info.session.access_token}`
if (info.session.logged_in) {
headers.Cookie = info.session.cookie;
headers.authorization = auth_creds;
}
return headers;
INNERTUBE_HEADERS_BASE: {
'accept': '*/*',
'content-type': 'application/json',
},
VIDEO_INFO_REQBODY: (id, sts, context) => {
return {
playbackContext: {
contentPlaybackContext: {
'currentUrl': '/watch?v=' + id,
'vis': 0,
'splay': false,
'autoCaptionsDefaultOn': false,
'autonavState': 'STATE_OFF',
'html5Preference': 'HTML5_PREF_WANTS',
'signatureTimestamp': sts,
'referer': 'https://www.youtube.com',
'lactMilliseconds': '-1'
}
},
context: context,
videoId: id
};
},
YTMUSIC_VERSION: '1.20211213.00.00',
METADATA_KEYS: [

@@ -131,7 +87,11 @@ 'embed', 'view_count', 'average_rating', 'allow_ratings',

},
SIG_REGEX: {
ACTIONS: /;.{2}\.(?<name>.{2})\(.*?,(?<param>.*?)\)/g,
FUNCTIONS: /(?<name>.{2}):function\(.*?\){(.*?)}/g
},
NTOKEN_REGEX: {
CALLS: /c\[(.*?)\]\((.+?)\)/g,
PLACEHOLDERS: /c\[(.*?)\]=c/g,
FUNCTIONS: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/
},
FUNCS_REGEX: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/,
FUNCS: {

@@ -150,2 +110,2 @@ PUSH: 'd.push(e)',

}
};
};
'use strict';
const Fs = require('fs');
const Crypto = require('crypto');

@@ -8,10 +7,13 @@ const UserAgent = require('user-agents');

function InnertubeError(message, info) {
this.info = info || {};
this.stack = Error(message).stack;
class InnertubeError extends Error {
constructor (message, info) {
super(message);
info && (this.info = info);
this.date = new Date();
this.version = require('../../package.json').version;
}
}
InnertubeError.prototype = Object.create(Error.prototype);
InnertubeError.prototype.constructor = InnertubeError;
class ParsingError extends InnertubeError {};

@@ -35,3 +37,3 @@ class DownloadError extends InnertubeError {};

const result = Object.keys(flat_obj).find((entry) => entry.includes(key) && JSON.stringify(flat_obj[entry] || '{}').includes(target));
if (!result) throw new ParsingError(`Expected to find "${key}" with content "${target}" but got ${result}`, { key, target, data_snippet: `${JSON.stringify(flat_obj).slice(0, 300)}..` });
if (!result) throw new ParsingError(`Expected to find "${key}" with content "${target}" but got ${result}`, { key, target, data_snippet: `${JSON.stringify(flat_obj, null, 4).slice(0, 300)}..` });
return flat_obj[result];

@@ -91,2 +93,13 @@ }

function generateRandomString(length) {
const result = [];
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
for (let i = 0; i < length; i++) {
result.push(alphabet.charAt(Math.floor(Math.random() * alphabet.length)));
}
return result.join('');
}
/**

@@ -133,5 +146,5 @@ * Converts time (h:m:s) to seconds.

const errors = { UnavailableContentError, ParsingError, DownloadError, InnertubeError, MissingParamError, NoStreamingDataError };
const functions = { findNode, getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
const errors = { InnertubeError, UnavailableContentError, ParsingError, DownloadError, MissingParamError, NoStreamingDataError };
const functions = { findNode, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
module.exports = { ...functions, ...errors };
{
"name": "youtubei.js",
"version": "1.4.1",
"version": "1.4.2-d.1",
"description": "A full-featured library that allows you to get detailed info about any video, subscribe, unsubscribe, like, dislike, comment, search, download videos/music and much more!",

@@ -13,3 +13,4 @@ "main": "index.js",

"scripts": {
"test": "node test"
"test": "node test",
"build:types": "npx tsc"
},

@@ -26,6 +27,10 @@ "types": "./typings/index.d.ts",

"flat": "^5.0.2",
"protons": "^2.0.3",
"protocol-buffers-encodings": "^1.1.1",
"user-agents": "^1.0.778",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/node": "^17.0.31",
"typescript": "^4.6.4"
},
"repository": {

@@ -58,2 +63,2 @@ "type": "git",

]
}
}

@@ -22,2 +22,6 @@ <h1 align=center>YouTube.js</h1>

</a>
<br>
<a href="https://ko-fi.com/luanrt">
<img src="https://img.shields.io/badge/donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white">
</a>
</p>

@@ -47,3 +51,3 @@

<li><a href="#live-chats">Livechats</a></li>
<li><a href="#downloading-videos">Downloading videos</a></li>
<li><a href="#download-videos">Download videos</a></li>
<li><a href="#signing-in">Signing in</a></li>

@@ -62,18 +66,15 @@ </ul>

Innertube is an API used across all YouTube clients, it was [made to simplify](https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491) the internal structure of the platform and make it easy to push updates. This library takes advantage of that API, therefore providing a simple & efficient way to interact with YouTube programmatically.
Innertube is an API used across all YouTube clients, it was created [to simplify](https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491) the internal structure of the platform in a way that updates, tweaks, and experiments can be easily made. This library handles all the low-level communication with Innertube, providing a simple and efficient way to interact with YouTube programmatically.
And big thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for his research on the workings of the Innertube API!
And huge thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for his research on the workings of the Innertube API!
### Features
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of its features:
- Search videos, playlists, music, albums etc.
- Subscribe/Unsubscribe/Like/Dislike/Comment etc.
- Get subscriptions/home feed, notifications and watch history.
- Search videos, playlists, music, albums, artists, etc.
- Subscribe, unsubscribe, like, dislike, post comments, replies, and etc.
- Get subscriptions/home feed, notifications, watch history, and more.
- Easily sign in to any Google Account.
- Fetch live chat & live stats.
- Manage account settings.
- Create/delete playlists.
- Manage playlists.
- Download videos.

@@ -83,3 +84,3 @@

Do note that you must be signed-in to perform actions that involve an account; such as commenting, liking/disliking videos, sending messages to a live chat, etc.
Do note that you must be signed in to perform actions that involve an account; such as commenting, liking/disliking videos, sending messages to a live chat, etc.

@@ -108,15 +109,20 @@ <!-- GETTING STARTED -->

```
- Git (bleeding-edge version):
```bash
npm install git+https://github.com/LuanRT/YouTube.js.git
```
<!-- USAGE -->
## Usage
First of all we're gonna start by initializing the Innertube instance.
And to make things faster, you should do this only once and reuse the Innertube object when needed.
Create an Innertube instance (or session):
```js
const Innertube = require('youtubei.js');
const youtube = await new Innertube();
const youtube = await new Innertube({ gl: 'US' }); // all parameters are optional.
```
### Doing a simple search
To improve performance, the Innertube instance should be initialized once and then reused throughout your program.
### A simple search:
YouTube:

@@ -254,3 +260,3 @@ ```js

### Get search suggestions:
### Search suggestions:
```js

@@ -278,3 +284,3 @@ const suggestions = await youtube.getSearchSuggestions('QUERY', {

### Get video info:
### Video info:

@@ -339,6 +345,7 @@ ```js

### Get comments:
### Comments
```js
const response = await youtube.getComments('VIDEO_ID');
// Sorting options: `TOP_COMMENTS` and `NEWEST_FIRST`
const comments = await youtube.getComments('VIDEO_ID', 'TOP_COMMENTS');
```

@@ -349,3 +356,3 @@ Alternatively you can use:

const video = await youtube.getDetails('VIDEO_ID');
const response = await video.getComments();
const comments = await video.getComments();
```

@@ -358,3 +365,5 @@ <details>

{
comments: [
page_count: number,
comment_count: number,
items: [
{

@@ -364,3 +373,3 @@ text: string,

name: string,
thumbnail: [
thumbnails: [
{

@@ -380,2 +389,3 @@ url: string,

is_channel_owner: boolean,
is_reply: boolean,
like_count: number,

@@ -387,4 +397,3 @@ reply_count: number,

//...
],
comment_count: string // not available in continuations
]
}

@@ -396,7 +405,13 @@ ```

Reply to, like and dislike comments:
Reply to, like/dislike, translate and report a comment:
```js
await response.comments[0].like();
await response.comments[0].dislike();
await response.comments[0].reply('Nice comment!');
const top_comment = comments.items[0];
await top_comment.like();
await top_comment.dislike();
await top_comment.report();
await top_comment.reply('Nice comment!');
// Note: only ISO language codes are accepted
await top_comment.translate('ru');
```

@@ -406,3 +421,3 @@

```js
const replies = await response.comments[0].getReplies();
const replies = await top_comment.getReplies();
```

@@ -412,7 +427,7 @@

```js
const continuation = await response.getContinuation();
const continuation = await comments.getContinuation();
const replies_continuation = await replies.getContinuation();
```
### Get home feed:
### Home feed:
```js

@@ -473,3 +488,3 @@ const homefeed = await youtube.getHomeFeed();

### Get watch history:
### Watch history:
```js

@@ -535,3 +550,3 @@ const history = await youtube.getHistory();

### Get subscriptions feed:
### Subscriptions feed:
```js

@@ -597,3 +612,3 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();

### Get trending content:
### Trending content:

@@ -629,3 +644,3 @@ ```js

### Get song lyrics:
### Song lyrics:
```js

@@ -636,3 +651,3 @@ const search = await youtube.search('Never give you up', { client: 'YTMUSIC' });

### Get notifications:
### Notifications:

@@ -782,23 +797,43 @@ ```js

```
* Playlists:
```js
// Create a playlist:
await youtube.playlist.create('NAME', 'VIDEO_ID');
* Playlists:
```js
const videos = [
'VIDEO_ID1',
'VIDEO_ID2',
'VIDEO_ID3'
//...
];
// Create and delete a playlist:
await youtube.playlist.create('My Playlist', videos);
await youtube.playlist.delete('PLAYLIST_ID');
// Delete a playlist:
await youtube.playlist.delete('PLAYLIST_ID');
// Add videos to a playlist:
await youtube.playlist.addVideos('PLAYLIST_ID', [ 'VIDEO_ID1', 'VIDEO_ID2' ]);
```
* Change notification preferences:
```js
// Options: ALL | NONE | PERSONALIZED
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL');
```
// Add and remove videos from a playlist:
await youtube.playlist.addVideos('PLAYLIST_ID', videos);
await youtube.playlist.removeVideos('PLAYLIST_ID', videos);
```
These methods will always return ```{ success: true, status_code: 200 }``` if successful.
* Translate (does not require sign in)
```js
await youtube.interact.translate('Hi mom!', 'ru');
```
* Change notification preferences:
```js
// Options: ALL | NONE | PERSONALIZED
await youtube.interact.setNotificationPreferences('CHANNEL_ID', 'ALL');
```
Response schema:
```js
{
success: boolean,
status_code: number,
playlist_id?: string,
translated_content?: string,
data?: object
}
```
### Account Settings

@@ -877,3 +912,3 @@ It is also possible to manage an account's settings:

YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats plus you can also send messages!
Currently, the library can retrieve live chat messages, stats and also send messages.

@@ -919,3 +954,3 @@ ```js

### Downloading videos:
### Download videos:
---

@@ -1050,3 +1085,3 @@

### Signing-in:
### Signing in:
---

@@ -1091,3 +1126,3 @@

```
Sign-out:
Sign out:
```js

@@ -1106,3 +1141,3 @@ const response = await youtube.signOut();

async function start() {
const youtube = await new Innertube(COOKIE_HERE);
const youtube = await new Innertube({ cookie: '...' });
//...

@@ -1120,2 +1155,7 @@ }

<!-- CONTRIBUTORS -->
## Contributors
<a href="https://github.com/LuanRT/YouTube.js/graphs/contributors">
<img src="https://contrib.rocks/image?repo=LuanRT/YouTube.js" />
</a>

@@ -1122,0 +1162,0 @@ <!-- CONTACT -->

@@ -0,1 +1,3 @@

// TODO: Refactor this to use a testing library
'use strict';

@@ -6,3 +8,3 @@

const NToken = require('../lib/deciphers/NToken');
const SigDecipher = require('../lib/deciphers/Sig');
const Signature = require('../lib/deciphers/Signature');
const Constants = require('./constants');

@@ -28,6 +30,6 @@

assert(!(ytsearch_suggestions instanceof Error), `should retrieve YouTube search suggestions`, ytsearch_suggestions);
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' }).catch((error) => error);
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search suggestions`, ytmsearch_suggestions);
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' });
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search suggestions`);
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);

@@ -45,5 +47,2 @@ assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details);

const lyrics = await youtube.getLyrics(Constants.test_song_id).catch((error) => error);
assert(!(lyrics instanceof Error), `should retrieve song lyrics`, lyrics);
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error);

@@ -56,4 +55,4 @@ assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video);

const transformed_url = new SigDecipher(Constants.test_url, { sig_decipher_sc: Constants.sig_decipher_sc }).decipher();
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`, transformed_url);
const transformed_url = new Signature(Constants.test_url, Constants.sig_decipher_sc).decipher();
assert(transformed_url == Constants.expected_url, `should decipher signature`, transformed_url);

@@ -77,3 +76,3 @@ if (failed_tests_count > 0)

const pass_fail = outcome ? 'pass' : 'fail';
console.info(pass_fail, ':', description);

@@ -80,0 +79,0 @@ !outcome && (failed_tests_count += 1);

@@ -1,181 +0,2 @@

interface AuthInfo {
access_token: string;
refresh_token: string;
expires: Date;
}
interface AccountInfo {
name: string;
photo: Record<string, any>[];
country: string;
language: string;
}
interface SearchOptions {
client: 'YTMUSIC' | 'YOUTUBE';
period: 'any' | 'hour' | 'day' | 'week' | 'month' | 'year';
order: 'relevance' | 'rating' | 'age' | 'views';
duration: 'any' | 'short' | 'long';
}
interface YouTubeSearch {
query: string;
corrected_query: string;
estimated_results: number;
videos: any[];
}
interface YouTubeMusicSearch {
query: string;
corrected_query: string;
results: {
top_result?: any[];
songs?: any[];
albums?: any[];
videos?: any[];
community_playlists?: any[];
artists?: any[];
}
}
type SearchResults = YouTubeSearch | YouTubeMusicSearch;
type ClientOption = Pick<SearchOptions, 'client'>;
interface Suggestion {
text: string;
bold_text: string;
}
interface ApiStatus {
success: boolean;
status_code: number;
data: object;
message?: string;
}
interface Comments {
comments: any[];
comment_count?: string;
}
interface Video {
title: string;
description: string;
thumbnail: object;
metadata: Record<any, any>;
like: () => Promise<ApiStatus>;
dislike: () => Promise<ApiStatus>;
removeLike: () => Promise<ApiStatus>;
subscribe: () => Promise<ApiStatus>;
unsubscribe: () => Promise<ApiStatus>;
comment: (text: string) => Promise<ApiStatus>;
getComments: () => Promise<Comments>;
getLivechat: () => any; // TODO type LiveChat
changeNotificationPreferences: () => Promise<ApiStatus>;
}
interface Channel {
title: string;
description: string;
metadata: object;
content: object;
}
interface PlayList {
description: string;
items: any[];
title: string;
total_items: string | number;
duration?: string;
last_updated?: string;
views?: string;
year?: string;
}
interface CommentData {
token: string;
channel_id: string;
}
interface History {
items: {
date: string;
videos: any[];
}[];
}
interface SubscriptionFeed {
items: {
date: string;
videos: any[];
}[];
}
interface HomeFeed {
videos: {
id: string;
title: string;
description: string;
channel: string;
metadata: Record<string, any>;
}[];
}
interface Trending {
now: {
content: {
title: string;
videos: [];
}[];
};
music: { getVideos: () => Promise<Array>; };
gaming: { getVideos: () => Promise<Array>; };
movies: { getVideos: () => Promise<Array>; };
}
interface Notifications {
items: {
title: string;
sent_time: string;
channel_name: string;
channel_thumbnail: Record<string, any>;
video_thumbnail: Record<string, any>;
video_url: string;
read: boolean;
notification_id: string;
}[];
}
interface StreamingData {
selected_format: Record<string, any>;
formats: any[];
}
interface StreamingOptions {
quality?: string;
type?: string;
format?: string;
}
export default class Innertube {
constructor(cookie?: string)
public signIn(auth_info: AuthInfo): Promise<void>;
public signOut(): Promise<ApiStatus>;
public getAccountInfo(): Promise<AccountInfo>;
public search(query: string, options: SearchOptions): Promise<SearchResults>;
public getSearchSuggestions(options: ClientOption): Promise<Suggestion>;
public getDetails(video_id: string): Promise<ApiStatus>;
public getChannel(id: string): Promise<Channel>;
public getLyrics(video_id: string): Promise<string>;
public getPlaylist(playlist_id: string, options?: ClientOption): Promise<PlayList>;
public getComments(video_id: string, options?: CommentData): Promise<Comments[]>;
public getHistory(): Promise<History>;
public getHomeFeed(): Promise<HomeFeed>;
public getTrending(): Promise<Trending>;
public getSubscriptionsFeed(): Promise<SubscriptionFeed>;
public getNotifications(): Promise<Notifications>;
public getUnseenNotificationsCount(): Promise<number>;
public getStreamingData(id: string, options?: StreamingOptions): Promise<StreamingData>;
public download(id: string, options?: StreamingOptions): ReadableStream;
}
declare const _exports: typeof import("./lib/Innertube");
export = _exports;
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc