mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-07-03 09:35:05 +00:00
refactor!: handle all request errors in Request.js and add debug mode
This commit is contained in:
@@ -6,483 +6,457 @@ const Proto = require('../proto');
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
|
||||
/**
|
||||
* Performs direct interactions on YouTube.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} engagement_type
|
||||
* @param {object} args
|
||||
* @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),
|
||||
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id),
|
||||
})[args.comment_action]();
|
||||
data.actions = [ action ];
|
||||
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.request.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 };
|
||||
/**
|
||||
* 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
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses YouTube's various sections.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @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))
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
|
||||
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
|
||||
|
||||
data.context = context;
|
||||
}
|
||||
|
||||
const response = await this.#request.post('/browse', JSON.stringify(data));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
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 target_action = ({
|
||||
like: () => Proto.encodeCommentActionParams(5, args.comment_id, args.video_id),
|
||||
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id),
|
||||
})[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');
|
||||
|
||||
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 'library':
|
||||
data.browseId = 'FElibrary';
|
||||
break;
|
||||
case 'trending':
|
||||
data.browseId = 'FEtrending';
|
||||
args.params && (data.params = args.params);
|
||||
break;
|
||||
case 'subscriptions_feed':
|
||||
data.browseId = 'FEsubscriptions';
|
||||
break;
|
||||
case 'channel':
|
||||
case 'playlist':
|
||||
data.browseId = args.browse_id;
|
||||
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';
|
||||
|
||||
data.context = context;
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
case 'continuation':
|
||||
data.continuation = args.ctoken;
|
||||
break;
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.request.post('/browse', 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoints used to report content.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function flag(session, action, args = {}) {
|
||||
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
|
||||
const data = { context: 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 session.request.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
|
||||
};
|
||||
}
|
||||
|
||||
async function playlist(session, action, args = {}) {
|
||||
const data = { context: 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 session.request.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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Account settings endpoints.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @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.request.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
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Covers the endpoint used for searches.
|
||||
*
|
||||
* @param {string} client
|
||||
* @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(client, args = {}) {
|
||||
const data = { context: this.#session.context };
|
||||
|
||||
switch (client) {
|
||||
case 'YOUTUBE':
|
||||
if (args.query) {
|
||||
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(this.#session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
/**
|
||||
* Accesses YouTube Music endpoints (/youtubei/v1/music/).
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @todo Implement more endpoints.
|
||||
* @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.request.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
|
||||
* @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) {
|
||||
data.params = Proto.encodeSearchFilter(args.options.period, args.options.duration, args.options.order);
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
|
||||
context.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
|
||||
|
||||
data.context = context;
|
||||
data.query = args.query;
|
||||
} else {
|
||||
break;
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid client', client);
|
||||
}
|
||||
|
||||
const response = await this.#request.post('/search', JSON.stringify(data));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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 'YTMUSIC':
|
||||
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
}
|
||||
|
||||
const response = await this.#request.post('/next', JSON.stringify(data));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve video info.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async getVideoInfo(id) {
|
||||
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
|
||||
};
|
||||
|
||||
const response = await this.#request.post('/player', JSON.stringify(data));
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: async () => 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;
|
||||
}
|
||||
|
||||
const response = await session.request.post('/search', 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interacts with YouTube's notification system.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @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);
|
||||
#needsLogin(id) {
|
||||
return [
|
||||
'FElibrary', 'FEhistory', 'FEsubscriptions',
|
||||
'SPaccount_notifications', 'SPaccount_privacy'
|
||||
].includes(id);
|
||||
}
|
||||
|
||||
const response = await session.request.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
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @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 = 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);
|
||||
}
|
||||
|
||||
const response = await session.request.post(`/${action}`, JSON.stringify(data)).catch((err) => err);
|
||||
if (response instanceof Error) return { success: false, message: response.message };
|
||||
|
||||
return { success: true, data: response.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests continuation for previously performed actions.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {object} args
|
||||
* @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);
|
||||
|
||||
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';
|
||||
|
||||
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 session.request.post('/next', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video data.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {object} args
|
||||
* @returns {Promise.<object>} - Video data.
|
||||
*/
|
||||
async function getVideoInfo(session, args = {}) {
|
||||
const response = await session.request.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
|
||||
* @param {string} 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);
|
||||
|
||||
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.config)).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])();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports = { engage, browse, account, playlist, flag, music, search, notifications, livechat, getVideoInfo, next, getSearchSuggestions };
|
||||
module.exports = Actions;
|
||||
Reference in New Issue
Block a user