mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-23 23:09:28 +00:00
feat: now it's possible to fetch the comments section
This commit is contained in:
@@ -6,7 +6,7 @@ const Constants = require('./Constants');
|
||||
const Uuid = require('uuid');
|
||||
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You must be signed-in to interact with a video/channel');
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data = {};
|
||||
switch (engagement_type) {
|
||||
case 'like/like':
|
||||
@@ -44,13 +44,34 @@ async function engage(session, engagement_type, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function browse(session, action_type) {
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'subscriptions_feed':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'FEsubscriptions'
|
||||
};
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
async function notifications(session, action_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You must be logged in to fetch notifications');
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'modify_channel_preference':
|
||||
let pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
|
||||
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()])
|
||||
@@ -70,7 +91,7 @@ async function notifications(session, action_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
return {
|
||||
@@ -83,7 +104,7 @@ async function notifications(session, action_type, args = {}) {
|
||||
async function livechat(session, action_type, args = {}) {
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'live_chat/send_message':
|
||||
case 'live_chat/send_message':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.generateMessageParams(args.channel_id, args.video_id),
|
||||
@@ -106,8 +127,8 @@ async function livechat(session, action_type, args = {}) {
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, params: args.params, desktop: true })).catch((error) => error);
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, params: args.params })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -135,7 +156,7 @@ async function getContinuation(session, info = {}) {
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -144,4 +165,4 @@ async function getContinuation(session, info = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { engage, notifications, livechat, getContinuation };
|
||||
module.exports = { engage, browse, notifications, livechat, getContinuation };
|
||||
@@ -64,10 +64,6 @@ const innertube_request_opts = (info) => {
|
||||
req_opts.headers.authorization = info.session.cookie.length < 1 ? `Bearer ${info.session.access_token}` : info.session.auth_apisid;
|
||||
}
|
||||
|
||||
if (info.data) {
|
||||
req_opts.headers['content-length'] = Buffer.byteLength(JSON.stringify(info.data), 'utf8');
|
||||
}
|
||||
|
||||
if (info.id) {
|
||||
req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id;
|
||||
}
|
||||
@@ -169,6 +165,7 @@ const formatVideoData = (data, context, desktop) => {
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.getComments = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
video_details.getLivechat = () => {};
|
||||
|
||||
@@ -178,8 +175,7 @@ const formatVideoData = (data, context, desktop) => {
|
||||
return video_details;
|
||||
};
|
||||
|
||||
const filters = (order) => {
|
||||
// TODO: Do this more efficiently
|
||||
const filters = (order) => { // TODO: Refactor this crazy thing
|
||||
switch (order) {
|
||||
case 'any,any,relevance':
|
||||
return 'EgIQAQ%3D%3D';
|
||||
|
||||
@@ -148,20 +148,109 @@ class Innertube extends EventEmitter {
|
||||
} else {
|
||||
video_data.getLivechat = () => {};
|
||||
}
|
||||
|
||||
|
||||
video_data.like = () => Actions.engage(this, 'like/like', { video_id: id });
|
||||
video_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: id });
|
||||
video_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id: id });
|
||||
video_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.comment = text => Actions.engage(this, 'comment/create_comment', { video_id: id, text });
|
||||
video_data.getComments = () => this.getComments(id);
|
||||
video_data.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: video_data.metadata.channel_id, pref: pref || 'NONE' });
|
||||
|
||||
return video_data;
|
||||
}
|
||||
|
||||
async getComments(video_id, token) {
|
||||
let comment_section_token;
|
||||
|
||||
if (!token) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id });
|
||||
const item_section_renderer = data_continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer);
|
||||
comment_section_token = item_section_renderer.itemSectionRenderer.contents[0].continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
}
|
||||
|
||||
const response = await Actions.getContinuation(this, { continuation_token: comment_section_token || token });
|
||||
if (!response.success) throw new Error('Could not fetch comment section');
|
||||
|
||||
const comments_section = { comments: [] };
|
||||
!token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A');
|
||||
|
||||
let continuation_token;
|
||||
!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);
|
||||
|
||||
comments_section.getContinuation = () => this.getComments(video_id, continuation_token);
|
||||
|
||||
let contents;
|
||||
!token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems)
|
||||
|| (contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
contents.forEach((thread) => {
|
||||
if (!thread.commentThreadRenderer) return;
|
||||
const comment = {
|
||||
text: thread.commentThreadRenderer.comment.commentRenderer.contentText.runs.map((t) => t.text).join(' '),
|
||||
author: {
|
||||
name: thread.commentThreadRenderer.comment.commentRenderer.authorText.simpleText,
|
||||
thumbnail: thread.commentThreadRenderer.comment.commentRenderer.authorThumbnail.thumbnails,
|
||||
channel_id: thread.commentThreadRenderer.comment.commentRenderer.authorEndpoint.browseEndpoint.browseId
|
||||
},
|
||||
metadata: {
|
||||
published: thread.commentThreadRenderer.comment.commentRenderer.publishedTimeText.runs[0].text,
|
||||
is_liked: thread.commentThreadRenderer.comment.commentRenderer.isLiked,
|
||||
is_channel_owner: thread.commentThreadRenderer.comment.commentRenderer.authorIsChannelOwner,
|
||||
like_count: thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText,
|
||||
reply_count: thread.commentThreadRenderer.comment.commentRenderer.replyCount || 0,
|
||||
id: thread.commentThreadRenderer.comment.commentRenderer.commentId,
|
||||
}
|
||||
};
|
||||
comments_section.comments.push(comment);
|
||||
});
|
||||
|
||||
return comments_section;
|
||||
}
|
||||
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await Actions.browse(this, 'subscriptions_feed');
|
||||
if (!response.success) throw new Error('Could not fetch subscriptions feed');
|
||||
|
||||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents;
|
||||
const subscriptions_feed = {};
|
||||
|
||||
contents.forEach((section) => {
|
||||
if (!section.itemSectionRenderer) return;
|
||||
|
||||
const section_contents = section.itemSectionRenderer.contents[0];
|
||||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
|
||||
|
||||
const key = section_contents.shelfRenderer.title.runs[0].text;
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')] = [];
|
||||
|
||||
section_items.forEach((item) => {
|
||||
const content = {
|
||||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
id: item.gridVideoRenderer.videoId,
|
||||
channel: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
metadata: {
|
||||
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A',
|
||||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails || [],
|
||||
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A',
|
||||
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
|
||||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
|
||||
}
|
||||
};
|
||||
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')].push(content);
|
||||
});
|
||||
});
|
||||
|
||||
return subscriptions_feed;
|
||||
}
|
||||
|
||||
async getNotifications() {
|
||||
const response = await Actions.notifications(this, 'get_notification_menu');
|
||||
if (!response.success) throw new Error('Could not fetch notifications');
|
||||
|
||||
const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0];
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' };
|
||||
return contents.multiPageMenuNotificationSectionRenderer.items.map((notification) => {
|
||||
@@ -182,6 +271,7 @@ class Innertube extends EventEmitter {
|
||||
|
||||
async getUnseenNotificationsCount() {
|
||||
const response = await Actions.notifications(this, 'get_unseen_count');
|
||||
if (!response.success) throw new Error('Could not fetch unseen notifications count');
|
||||
return response.data.unseenCount;
|
||||
}
|
||||
|
||||
@@ -326,10 +416,10 @@ class Innertube extends EventEmitter {
|
||||
const downloadChunk = async () => {
|
||||
if (chunk_end >= selected_format.contentLength) end = true;
|
||||
|
||||
const response = await Axios.get(selected_format.url, {
|
||||
const response = await Axios.get(`${selected_format.url}&range=${chunk_start}-${chunk_end || ''}`, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.stream_headers(`bytes=${chunk_start}-${chunk_end || ''}`)
|
||||
headers: Constants.stream_headers()
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) {
|
||||
|
||||
@@ -21,7 +21,7 @@ class Livechat extends EventEmitter {
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
|
||||
enqueueActionGroup(group) {
|
||||
group.forEach((action) => {
|
||||
if (!action.addChatItemAction) return; //TODO: handle different action types */
|
||||
@@ -38,7 +38,7 @@ class Livechat extends EventEmitter {
|
||||
timestamp: message_content.timestampUsec,
|
||||
id: message_content.id
|
||||
};
|
||||
|
||||
|
||||
this.message_queue.push(message);
|
||||
});
|
||||
}
|
||||
@@ -62,9 +62,9 @@ class Livechat extends EventEmitter {
|
||||
setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime());
|
||||
this.id_cache.push(message.id);
|
||||
});
|
||||
|
||||
|
||||
this.message_queue = [];
|
||||
|
||||
|
||||
data = { context: this.session.context, videoId: this.video_id };
|
||||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
|
||||
|
||||
@@ -81,23 +81,23 @@ class Livechat extends EventEmitter {
|
||||
short_view_count: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.extraShortViewCount.simpleText
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.livechat_poller = setTimeout(async () => await this.poll(), this.poll_intervals_ms);
|
||||
}
|
||||
|
||||
|
||||
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 });
|
||||
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 } });
|
||||
if (!menu.success) return menu;
|
||||
|
||||
|
||||
const chat_item_menu = menu.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
|
||||
const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
|
||||
if (!cmd.success) return cmd;
|
||||
|
||||
|
||||
return { success: true, status_code: cmd.status_code };
|
||||
};
|
||||
|
||||
@@ -117,9 +117,9 @@ class Livechat extends EventEmitter {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async blockUser(msg_params) {
|
||||
/* TODO: Implement this */
|
||||
/* TODO: Implement this */
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
|
||||
12
lib/Utils.js
12
lib/Utils.js
@@ -43,7 +43,7 @@ function createFunction(input, raw_code) { // I hate this
|
||||
|
||||
function encodeNotificationPref(channel_id, index) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
|
||||
const buf = youtube_proto.NotificationPreferences.encode({
|
||||
channel_id,
|
||||
pref_id: {
|
||||
@@ -52,24 +52,24 @@ function encodeNotificationPref(channel_id, index) {
|
||||
number_0: 0,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
function generateMessageParams(channel_id, video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
|
||||
const buf = youtube_proto.LiveMessageParams.encode({
|
||||
params: {
|
||||
ids: {
|
||||
channel_id,
|
||||
video_id
|
||||
channel_id,
|
||||
video_id
|
||||
}
|
||||
},
|
||||
number_0: 1,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user