youtubei.js
Advanced tools
Comparing version 1.1.1 to 1.2.1
@@ -6,5 +6,6 @@ 'use strict'; | ||
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 = {}; | ||
@@ -33,3 +34,3 @@ switch (engagement_type) { | ||
commentText: args.text, | ||
createCommentParams: Utils.encodeVideoId(args.video_id) | ||
createCommentParams: Utils.generateCommentParams(args.video_id) | ||
}; | ||
@@ -48,11 +49,33 @@ break; | ||
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 = { ALL: 0, NONE: 1, PERSONALIZED: 2 }; | ||
let pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 }; | ||
data = { | ||
context: session.context, | ||
params: Utils.encodeChannelId(args.channel_id, pref_types[args.pref.toUpperCase()]) | ||
params: Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]) | ||
}; | ||
@@ -74,3 +97,3 @@ break; | ||
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 }; | ||
@@ -85,2 +108,38 @@ if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status }; | ||
async function livechat(session, action_type, args = {}) { | ||
let data; | ||
switch (action_type) { | ||
case 'live_chat/send_message': | ||
data = { | ||
context: session.context, | ||
params: Utils.generateMessageParams(args.channel_id, args.video_id), | ||
clientMessageId: `INntLiB${Uuid.v4()}`, | ||
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, | ||
params: args.cmd_params | ||
}; | ||
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, params: args.params })).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 getContinuation(session, info = {}) { | ||
@@ -105,3 +164,3 @@ let data = { context: session.context }; | ||
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 }; | ||
@@ -115,2 +174,2 @@ return { | ||
module.exports = { engage, notifications, getContinuation }; | ||
module.exports = { engage, browse, notifications, livechat, getContinuation }; |
@@ -46,2 +46,3 @@ 'use strict'; | ||
let req_opts = { | ||
params: info.params || {}, | ||
headers: { | ||
@@ -67,6 +68,2 @@ 'accept': '*/*', | ||
if (info.data) { | ||
req_opts.headers['content-length'] = Buffer.byteLength(JSON.stringify(info.data), 'utf8'); | ||
} | ||
if (info.id) { | ||
@@ -166,3 +163,3 @@ req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id; | ||
// Actions | ||
// Functions | ||
video_details.like = () => {}; | ||
@@ -174,6 +171,5 @@ video_details.dislike = () => {}; | ||
video_details.comment = () => {}; | ||
video_details.getComments = () => {}; | ||
video_details.setNotificationPref = () => {}; | ||
if (metadata.is_live_content) { | ||
video_details.getLivechat = () => {}; | ||
} | ||
video_details.getLivechat = () => {}; | ||
@@ -186,2 +182,8 @@ // Additional metadata | ||
const base64_alphabet = { | ||
normal: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''), | ||
reverse: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('') | ||
}; | ||
const filters = (order) => { // TODO: Refactor this crazy thing | ||
@@ -337,2 +339,2 @@ switch (order) { | ||
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, filters }; | ||
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, base64_alphabet, filters }; |
@@ -8,2 +8,3 @@ 'use strict'; | ||
const Player = require('./Player'); | ||
const NToken = require('./NToken'); | ||
const Actions = require('./Actions'); | ||
@@ -149,4 +150,6 @@ const Livechat = require('./Livechat'); | ||
video_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, video_data.metadata.channel_id, id); | ||
} else { | ||
video_data.getLivechat = () => {}; | ||
} | ||
video_data.like = () => Actions.engage(this, 'like/like', { video_id: id }); | ||
@@ -158,2 +161,3 @@ video_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: 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' }); | ||
@@ -163,5 +167,93 @@ | ||
} | ||
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]; | ||
@@ -187,2 +279,3 @@ if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' }; | ||
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; | ||
@@ -224,3 +317,3 @@ } | ||
if (format.signatureCipher || format.cipher) { | ||
format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player.sig_decipher_sc, this.player.encodeN).decipher(); | ||
format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player).decipher(); | ||
} else { | ||
@@ -230,3 +323,3 @@ const url_components = new URL(format.url); | ||
url_components.searchParams.set('ratebypass', 'yes'); | ||
url_components.searchParams.set('n', this.player.encodeN(url_components.searchParams.get('n'))); | ||
url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n'))); | ||
format.url = url_components.toString(); | ||
@@ -291,3 +384,8 @@ } | ||
if (options.type == 'videoandaudio') { | ||
const response = await Axios.get(selected_format.url, { cancelToken: new CancelToken(function executor(c) { cancel = c; }), responseType: 'stream', reponseEncoding: 'binary', headers: Constants.stream_headers() }).catch((error) => error); | ||
const response = await Axios.get(selected_format.url, { | ||
responseType: 'stream', | ||
cancelToken: new CancelToken(function executor(c) { cancel = c; }), | ||
headers: Constants.stream_headers() | ||
}).catch((error) => error); | ||
if (response instanceof Error) { | ||
@@ -316,5 +414,3 @@ stream.emit('error', { message: response.message, type: 'REQUEST_FAILED' }); | ||
response.data.on('end', () => setTimeout(() => stream.emit('end'), 500)); | ||
response.data.pipe(stream, { end: false }); | ||
response.data.pipe(stream, { end: true }); | ||
} else { | ||
@@ -326,2 +422,3 @@ const chunk_size = 1048576 * 10; // 10MB | ||
let downloaded_size = 0; | ||
let end = false; | ||
@@ -331,3 +428,10 @@ stream.emit('start'); | ||
const downloadChunk = async () => { | ||
const response = await Axios.get(selected_format.url, { cancelToken: new CancelToken(function executor(c) { cancel = c; }), responseType: 'stream', headers: Constants.stream_headers(`bytes=${chunk_start}-${chunk_end || ''}`) }).catch((error) => error); | ||
if (chunk_end >= selected_format.contentLength) end = true; | ||
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() | ||
}).catch((error) => error); | ||
if (response instanceof Error) { | ||
@@ -354,13 +458,10 @@ stream.emit('error', { message: response.message, type: 'REQUEST_FAILED' }); | ||
response.data.on('end', () => { | ||
chunk_start = chunk_end + 1; | ||
chunk_end += chunk_size; | ||
if (downloaded_size < selected_format.contentLength) { | ||
if (!end) { | ||
chunk_start = chunk_end + 1; | ||
chunk_end += chunk_size; | ||
downloadChunk(); | ||
} else { | ||
stream.emit('end'); | ||
} | ||
}); | ||
response.data.pipe(stream, { end: false }); | ||
response.data.pipe(stream, { end }); | ||
}; | ||
@@ -367,0 +468,0 @@ downloadChunk(); |
'use strict'; | ||
const Axios = require('axios'); | ||
const Utils = require('./Utils'); | ||
const Actions = require('./Actions'); | ||
const Constants = require('./Constants'); | ||
const EventEmitter = require('events'); | ||
const Uuid = require("uuid"); | ||
@@ -20,3 +19,3 @@ class Livechat extends EventEmitter { | ||
this.poll_intervals_ms = 0; | ||
this.poll_intervals_ms = 1000; | ||
this.running = true; | ||
@@ -26,56 +25,6 @@ | ||
} | ||
async sendMessage(text) { | ||
let data = { | ||
context: this.session.context, | ||
params: Utils.encodeChannelIdWithVideoId(this.channel_id, this.video_id), | ||
clientMessageId: `INntLiB${Uuid.v4()}`, | ||
richMessage: { | ||
textSegments: [{ text }] | ||
} | ||
}; | ||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/send_message${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, id: this.video_id, desktop: true })).catch((error) => error); | ||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.response.data.error.message }; | ||
const deleteMessage = async () => { | ||
/* | ||
* The first request is made to get the chat options and the delete command endpoint, | ||
* these options contain the required params to delete a message (a string composed of clientId, the channelId of the channel you're watching, your public channelId and the id of the message you sent). | ||
* All put together with some binary data and then base64ed twice (yes, twice lm*o top notch security). | ||
**/ | ||
const item_menu_res = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_item_context_menu?params=${response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params}&pbj=1${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify({ context: this.session.context }), Constants.innertube_request_opts({ session: this.session, id: this.video_id, desktop: true })).catch((error) => error); | ||
if (item_menu_res instanceof Error) return { success: false, status_code: item_menu_res.response.status, message: item_menu_res.response.data.error.message }; | ||
const chat_item_menu = item_menu_res.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0]; | ||
const delete_message_reqbody = { | ||
context: this.session.context, | ||
params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params | ||
}; | ||
const delete_message_cmd = await Axios.post(`${Constants.urls.YT_BASE_URL}${chat_item_menu.menuServiceItemRenderer.serviceEndpoint.commandMetadata.webCommandMetadata.apiUrl}${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify(delete_message_reqbody), Constants.innertube_request_opts({ session: this.session, delete_message_reqbody, id: this.video_id, desktop: true })).catch((error) => error); | ||
if (delete_message_cmd instanceof Error) return { success: false, status_code: delete_message_cmd.response.status, message: delete_message_cmd.response.data.error.message }; | ||
return { success: true, status_code: response.status }; | ||
}; | ||
return { | ||
success: true, | ||
status_code: response.status, | ||
deleteMessage: () => deleteMessage(), | ||
message_data: { | ||
text: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '), | ||
author: { | ||
name: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/', | ||
channel_id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId, | ||
profile_picture: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails | ||
}, | ||
timestamp: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec, | ||
id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id | ||
} | ||
}; | ||
} | ||
enqueueActionGroup(group) { | ||
group.forEach((action) => { | ||
if (!action.addChatItemAction) return; | ||
if (!action.addChatItemAction) return; //TODO: handle different action types */ | ||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer; | ||
@@ -94,3 +43,3 @@ if (!message_content) return; | ||
}; | ||
this.message_queue.push(message); | ||
@@ -138,13 +87,43 @@ }); | ||
}); | ||
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 }; | ||
}; | ||
// How long we should wait to poll the chat again. | ||
if (continuation_contents.liveChatContinuation.continuations[0].timedContinuationData) { | ||
this.poll_intervals_ms = continuation_contents.liveChatContinuation.continuations[0].timedContinuationData.timeoutMs; | ||
} else { | ||
this.poll_intervals_ms = 4000; | ||
} | ||
await this.poll(); | ||
this.livechat_poller = setTimeout(() => this.poll(), this.poll_intervals_ms); | ||
return { | ||
success: true, | ||
status_code: message.status_code, | ||
deleteMessage, | ||
message_data: { | ||
text: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '), | ||
author: { | ||
name: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/', | ||
channel_id: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId, | ||
profile_picture: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails | ||
}, | ||
timestamp: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec, | ||
id: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id | ||
} | ||
}; | ||
} | ||
async blockUser(msg_params) { | ||
/* TODO: Implement this */ | ||
throw new Error('Not implemented'); | ||
} | ||
@@ -151,0 +130,0 @@ stop() { |
@@ -39,4 +39,3 @@ 'use strict'; | ||
getNEncoder(data) { | ||
const raw_code = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");'; | ||
this.encodeN = Utils.createFunction('a', raw_code); | ||
this.ntoken_sc = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");'; | ||
} | ||
@@ -43,0 +42,0 @@ } |
'use strict'; | ||
const NToken = require('./NToken'); | ||
const QueryString = require('querystring'); | ||
class SigDecipher { | ||
constructor(url, cver, func_code, encode_n) { | ||
constructor(url, cver, player) { | ||
this.url = url; | ||
this.cver = cver; | ||
this.func_code = func_code; | ||
this.encode_n = encode_n; | ||
this.player = player; | ||
this.func_regex = /(.{2}):function\(.*?\){(.*?)}/g; | ||
@@ -36,3 +36,3 @@ this.actions_regex = /;.{2}\.(.{2})\(.*?,(.*?)\)/g; | ||
while ((actions = this.actions_regex.exec(this.func_code)) !== null) { | ||
while ((actions = this.actions_regex.exec(this.player.sig_decipher_sc)) !== null) { | ||
switch (actions[1]) { | ||
@@ -53,6 +53,7 @@ case functions[0]: | ||
const url_components = new URL(args.url); | ||
args.sp !== undefined ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join('')); | ||
url_components.searchParams.set('cver', this.cver); | ||
url_components.searchParams.set('ratebypass', 'yes'); | ||
url_components.searchParams.set('n', this.encode_n(url_components.searchParams.get('n'))); | ||
url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n'))); | ||
return url_components.toString(); | ||
@@ -65,3 +66,3 @@ } | ||
while ((func = this.func_regex.exec(this.func_code)) !== null) { | ||
while ((func = this.func_regex.exec(this.player.sig_decipher_sc)) !== null) { | ||
if (func[0].includes('reverse()')) { | ||
@@ -68,0 +69,0 @@ func_name[0] = func[1]; |
'use strict'; | ||
const Fs = require('fs'); | ||
const Proto = require('protons'); | ||
const Crypto = require('crypto'); | ||
@@ -38,35 +40,48 @@ const UserAgent = require('user-agents'); | ||
function createFunction(input, raw_code) { // I hate this | ||
return new Function(input, raw_code); | ||
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: { | ||
index | ||
}, | ||
number_0: 0, | ||
number_1: 4 | ||
}); | ||
return encodeURIComponent(Buffer.from(buf).toString('base64')); | ||
} | ||
function encodeVideoId(id) { | ||
return encodeURIComponent(`${Buffer.from(`` + id + `*`).toString('base64').slice(0, -1)}BQBw==`); | ||
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 | ||
} | ||
}, | ||
number_0: 1, | ||
number_1: 4 | ||
}); | ||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64'); | ||
} | ||
function encodeChannelId(id, notification_pref) { | ||
const buff_start = ` | ||
`; | ||
const buff_end = [ | ||
``, // all | ||
``, // none | ||
``, // personalized | ||
]; | ||
function generateCommentParams(video_id) { | ||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
let encodedId = Buffer.from([buff_start, id, buff_end[notification_pref]].join('')).toString('base64'); | ||
return encodeURIComponent(`${encodedId}GAAgBA==`); | ||
} | ||
const buf = youtube_proto.CreateCommentParams.encode({ | ||
video_id, | ||
params: { | ||
index: 0 | ||
}, | ||
number: 7 | ||
}); | ||
function encodeChannelIdWithVideoId(channel_id, video_id) { | ||
const buff_start = ` | ||
)*' | ||
`; | ||
const buff_middle = ``; | ||
const buff_end = ``; | ||
// Yes, we also have to base64 these twice lol | ||
let encodedIds = Buffer.from([buff_start, channel_id, buff_middle, video_id, buff_end].join('')).toString('base64'); | ||
return `${Buffer.from(encodedIds).toString('base64').slice(0, -4)}JTNE`; | ||
return encodeURIComponent(Buffer.from(buf).toString('base64')); | ||
} | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeChannelIdWithVideoId, encodeVideoId, encodeChannelId }; | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref }; |
{ | ||
"name": "youtubei.js", | ||
"version": "1.1.1", | ||
"version": "1.2.1", | ||
"description": "An object-oriented library that allows you to search, get detailed info about videos, subscribe, unsubscribe, like, dislike, comment, download videos and much more!", | ||
@@ -17,2 +17,3 @@ "main": "index.js", | ||
"axios": "^0.21.4", | ||
"protons": "^2.0.3", | ||
"time-to-seconds": "^1.1.5", | ||
@@ -37,4 +38,5 @@ "user-agents": "^1.0.778", | ||
"comment", | ||
"automation", | ||
"downloader", | ||
"automation", | ||
"comments-section", | ||
"youtube-downloader" | ||
@@ -41,0 +43,0 @@ ], |
300
README.md
@@ -11,14 +11,15 @@ # YouTube.js | ||
As of now, this is one of the most advanced & stable YouTube libraries out there, and it can: | ||
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of what it can do: | ||
- Search | ||
- Get detailed info about videos | ||
- Search videos | ||
- Get detailed info about any video | ||
- Fetch live chat & live stats in real time | ||
- Fetch notifications | ||
- Fetch subscriptions feed | ||
- Change notifications preferences for a channel | ||
- Subscribe/Unsubscribe/Like/Dislike/Comment | ||
- Easily sign into your account without having to use cookies! | ||
- Easily sign into your account in an easy & reliable way. | ||
- Last but not least, you can also download videos! | ||
Do note that you must be signed-in to perform actions that involve an account, like commenting, subscribing, 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, subscribing, sending messages to a live chat, etc. | ||
@@ -37,13 +38,13 @@ #### Do I need an API key to use this? | ||
[1. Basic Usage](https://www.npmjs.com/package/youtubei.js#usage) | ||
[1. Basic Usage](https://github.com/LuanRT/YouTube.js#usage) | ||
[2. Interactions](https://www.npmjs.com/package/youtubei.js#interactions) | ||
[2. Interactions](https://github.com/LuanRT/YouTube.js#interactions) | ||
[3. Fetching live chats](https://www.npmjs.com/package/youtubei.js#fetching-live-chats) | ||
[3. Fetching live chats](https://github.com/LuanRT/YouTube.js#fetching-live-chats) | ||
[4. Downloading videos](https://www.npmjs.com/package/youtubei.js#downloading-videos) | ||
[4. Downloading videos](https://github.com/LuanRT/YouTube.js#downloading-videos) | ||
[5. Signing-in](https://www.npmjs.com/package/youtubei.js#signing-in) | ||
[5. Signing-in](https://github.com/LuanRT/YouTube.js#signing-in) | ||
[6. Disclaimer](https://www.npmjs.com/package/youtubei.js#disclaimer) | ||
[6. Disclaimer](https://github.com/LuanRT/YouTube.js#disclaimer) | ||
@@ -192,6 +193,269 @@ First of all we're gonna start by initializing the Innertube class: | ||
Getting comments: | ||
Fetching notifications: | ||
```js | ||
const video = await youtube.getDetails(VIDEO_ID_HERE); | ||
const comments = await video.getComments(); | ||
// If you want to load more comments simply call: | ||
const comments_continuation = await comments.getContinuation(); | ||
``` | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
```js | ||
{ | ||
"comments":[ | ||
{ | ||
"text":"The amazing thing to me is the engineering. It's truly remarkable that we can build machines like these.", | ||
"author":{ | ||
"name":"Mark B", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s48-c-k-c0x00ffffff-no-rj", | ||
"width":48, | ||
"height":48 | ||
}, | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s88-c-k-c0x00ffffff-no-rj", | ||
"width":88, | ||
"height":88 | ||
}, | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s176-c-k-c0x00ffffff-no-rj", | ||
"width":176, | ||
"height":176 | ||
} | ||
], | ||
"channel_id":"UClnPXUOtCLnKsbS2reuN7wg" | ||
}, | ||
"metadata":{ | ||
"published":"2 months ago", | ||
"is_liked":false, | ||
"is_channel_owner":false, | ||
"like_count":"54", | ||
"reply_count":3, | ||
"id":"Ugy-bGGepYil_2dAQUp4AaABAg" | ||
} | ||
}, | ||
{ | ||
"text":"May 25th, 2021 and everything has gone perfectly! Ingenuity, moxy and perseverance all working to plan. Unbelievable accomplishments!!!", | ||
"author":{ | ||
"name":"cliff luebke", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s48-c-k-c0x00ffffff-no-rj", | ||
"width":48, | ||
"height":48 | ||
}, | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s88-c-k-c0x00ffffff-no-rj", | ||
"width":88, | ||
"height":88 | ||
}, | ||
{ | ||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s176-c-k-c0x00ffffff-no-rj", | ||
"width":176, | ||
"height":176 | ||
} | ||
], | ||
"channel_id":"UCeVFeX4jCgaJpvNJ4f_I4RA" | ||
}, | ||
"metadata":{ | ||
"published":"4 months ago", | ||
"is_liked":false, | ||
"is_channel_owner":false, | ||
"like_count":"54", | ||
"reply_count":0, | ||
"id":"UgylkZHOe7v78hxHPpl4AaABAg" | ||
} | ||
}, | ||
//... | ||
], | ||
"comment_count":"3,231" // not available in continuations | ||
} | ||
``` | ||
</p> | ||
</details> | ||
Getting subscriptions feed: | ||
```js | ||
const mysubfeed = await youtube.getSubscriptionsFeed(); | ||
``` | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
```js | ||
{ | ||
"today":[ | ||
{ | ||
"title":"Life As My P*nis", | ||
"id":"udDINILQH10", | ||
"channel":"penguinz0", | ||
"metadata":{ | ||
"view_count":"220,432 views", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAQ7bbeUhCxRSg-g-CPek-soixUMQ", | ||
"width":210, | ||
"height":118 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB6fuvBJMeLtkM0TLkZwharsyojjA", | ||
"width":246, | ||
"height":138 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDd7BncH1QuZD-Hada_n6dAVRTnmg", | ||
"width":336, | ||
"height":188 | ||
} | ||
], | ||
"published":"2 hours ago", | ||
"badges":"N/A", | ||
"owner_badges":[ | ||
"Verified" | ||
] | ||
} | ||
}, | ||
{ | ||
"title":"Perseverance and Ingenuity went two weeks without contacting Earth", | ||
"id":"VsmYZMVCHuc", | ||
"channel":"Mars Guy", | ||
"metadata":{ | ||
"view_count":"2,633 views", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA0-vwAgpFbMO1zG4HTzdHZey1kZQ", | ||
"width":210, | ||
"height":118 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBtM3W-RXsCfPnxgnrktaBkiL9zzg", | ||
"width":246, | ||
"height":138 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDTil7At4FUVYeSNySOoFoKlPXWSA", | ||
"width":336, | ||
"height":188 | ||
} | ||
], | ||
"published":"15 hours ago", | ||
"badges":"N/A", | ||
"owner_badges":"N/A" | ||
} | ||
} | ||
//... | ||
], | ||
"yesterday":[ | ||
{ | ||
"title":"Fortnite - S.T.A.R.S (Resident Evil) | PS5, PS4", | ||
"id":"-ZLEQOVbWD4", | ||
"channel":"PlayStation", | ||
"metadata":{ | ||
"view_count":"157,197 views", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAeA1fLzsEA0ZIouNJuMDJOqOc9Ng", | ||
"width":210, | ||
"height":118 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDzvhxJ6m2ztykk2ezNH2Din33hEw", | ||
"width":246, | ||
"height":138 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDo67DvnaiKzOVfNm9lJg_Edd1UDQ", | ||
"width":336, | ||
"height":188 | ||
} | ||
], | ||
"published":"1 day ago", | ||
"badges":"N/A", | ||
"owner_badges":[ | ||
"Verified" | ||
] | ||
} | ||
}, | ||
//... | ||
], | ||
"this_week":[ | ||
{ | ||
"title":"Horrible $100 Million Gold Mansion", | ||
"id":"F-d3CEYJyrg", | ||
"channel":"penguinz0", | ||
"metadata":{ | ||
"view_count":"693,041 views", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDdMMGG-50O4U5uIcoZ2FoiO6Mopg", | ||
"width":210, | ||
"height":118 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAwqMy1ekLaxWGnjWwCJl7z7Nw2aQ", | ||
"width":246, | ||
"height":138 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLADaPBg0vh52e6clHvUf5otBZO9HA", | ||
"width":336, | ||
"height":188 | ||
} | ||
], | ||
"published":"2 days ago", | ||
"badges":"N/A", | ||
"owner_badges":[ | ||
"Verified" | ||
] | ||
} | ||
}, | ||
{ | ||
"title":"OOopsieeee", | ||
"id":"mJ2WOIhEPm8", | ||
"channel":"PewDiePie", | ||
"metadata":{ | ||
"view_count":"1,953,970 views", | ||
"thumbnail":[ | ||
{ | ||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCrJe2a6rasJICj_jchMquZ2YGVrQ", | ||
"width":210, | ||
"height":118 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCpxERsiLJayuKegeb5mHw3Ok6wGA", | ||
"width":246, | ||
"height":138 | ||
}, | ||
{ | ||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAlRLjypimzrE3GD_iGYDGwTlCGvA", | ||
"width":336, | ||
"height":188 | ||
} | ||
], | ||
"published":"2 days ago", | ||
"badges":"N/A", | ||
"owner_badges":[ | ||
"Verified" | ||
] | ||
} | ||
}, | ||
//... | ||
] | ||
} | ||
``` | ||
</p> | ||
</details> | ||
Getting notifications: | ||
```js | ||
const notifications = await youtube.getNotifications(); | ||
@@ -266,3 +530,2 @@ ``` | ||
### Fetching live chats: | ||
@@ -292,3 +555,3 @@ --- | ||
If(message.text == '!info') { | ||
if(message.text == '!info') { | ||
livechat.sendMessage('Hello! This message was sent from YouTube.js'); | ||
@@ -301,2 +564,7 @@ } | ||
``` | ||
Stop fetching the live chat: | ||
```js | ||
livechat.stop(); | ||
``` | ||
Deleting a message: | ||
@@ -311,2 +579,4 @@ ```js | ||
The library provides an easy-to-use and simple downloader: | ||
```js | ||
@@ -366,3 +636,3 @@ const fs = require('fs'); | ||
OAuth 2.0: | ||
OAuth: | ||
@@ -369,0 +639,0 @@ ```js |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
88830
13
1392
691
0
5
2
+ Addedprotons@^2.0.3
+ Addedmultiformats@9.9.0(transitive)
+ Addedprotocol-buffers-schema@3.6.0(transitive)
+ Addedprotons@2.0.3(transitive)
+ Addedsigned-varint@2.0.1(transitive)
+ Addeduint8arrays@3.1.1(transitive)
+ Addedvarint@5.0.2(transitive)