Compare commits

..

9 Commits

Author SHA1 Message Date
luan.lrt4@gmail.com
db41fa40d2 chore: bump version to 1.4.2 2022-04-22 00:53:05 -03:00
luan.lrt4@gmail.com
02ece1ddda chore: fix typo 2022-04-22 00:32:43 -03:00
luan.lrt4@gmail.com
b175e02f6d chore: oops 2022-04-22 00:27:03 -03:00
luan.lrt4@gmail.com
d3394f846a feat: add support for reporting comments and add comments sorting option 2022-04-22 00:22:50 -03:00
luan.lrt4@gmail.com
07b73ab78d chore: remove unneeded code 2022-04-20 06:19:36 -03:00
luan.lrt4@gmail.com
d743b5a088 refactor: use a single axios instance and remove redundant code 2022-04-20 06:18:07 -03:00
luan.lrt4@gmail.com
bb206c044c chore(tests): update signature decipher path 2022-04-20 03:55:14 -03:00
luan.lrt4@gmail.com
d48065405d chore: use compiled protobuf schemas to reduce dependency footprint 2022-04-20 03:52:44 -03:00
luan.lrt4@gmail.com
dbc8b62ba2 feat: add option to change geolocation & fix minor bugs, closes #34 2022-04-19 05:35:11 -03:00
19 changed files with 2428 additions and 606 deletions

View File

@@ -109,7 +109,7 @@ And to make things faster, you should do this only once and reuse the Innertube
```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
@@ -332,13 +332,14 @@ const video = await youtube.getDetails('VIDEO_ID');
### Get 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');
```
Alternatively you can use:
```js
const video = await youtube.getDetails('VIDEO_ID');
const response = await video.getComments();
const comments = await video.getComments();
```
<details>
<summary>Output</summary>
@@ -346,12 +347,14 @@ const response = await video.getComments();
```js
{
comments: [
page_count: number,
comment_count: number,
items: [
{
text: string,
author: {
name: string,
thumbnail: [
thumbnails: [
{
url: string,
width: number,
@@ -366,35 +369,36 @@ const response = await video.getComments();
is_disliked: boolean,
is_pinned: boolean,
is_channel_owner: boolean,
is_reply: boolean,
like_count: number,
reply_count: number,
id: string
}
},
//...
],
comment_count: string // not available in continuations
]
}
```
</p>
</details>
Reply to, like and dislike comments:
Reply to, like/dislike and report a comment:
```js
await response.comments[0].like();
await response.comments[0].dislike();
await response.comments[0].reply('Nice comment!');
await comments.items[0].like();
await comments.items[0].dislike();
await comments.items[0].report();
await comments.items[0].reply('Nice comment!');
```
Comment replies:
```js
const replies = await response.comments[0].getReplies();
const replies = await comments.items[0].getReplies();
```
Comments/replies continuation:
```js
const continuation = await response.getContinuation();
const continuation = await comments.getContinuation();
const replies_continuation = await replies.getContinuation();
```
@@ -774,7 +778,7 @@ The library makes it easy to interact with YouTube programmatically. However, do
* Change notification preferences:
```js
// Options: ALL | NONE | PERSONALIZED
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL');
await youtube.interact.setNotificationPreferences('CHANNEL_ID', 'ALL');
```
These methods will always return ```{ success: true, status_code: 200 }``` if successful.
@@ -1077,7 +1081,7 @@ if (response.success) {
const Innertube = require('youtubei.js');
async function start() {
const youtube = await new Innertube(COOKIE_HERE);
const youtube = await new Innertube({ cookie: '...' });
//...
}

View File

@@ -12,34 +12,38 @@ const Actions = require('./core/Actions');
const Livechat = require('./core/Livechat');
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');
class Innertube {
#oauth;
#player;
#retry_count;
/**
* ```js
* const Innertube = require('youtubei.js');
* const youtube = await new Innertube();
* ```
* @param {string} [cookie]
* @param {object} [config]
* @param {string} [config.gl]
* @param {string} [config.cookie]
* @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 response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this.config)).catch((error) => error);
if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve Innertube session', { message: response.message, status_code: response.status || 0 });
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') || ''}}`);
if (data.INNERTUBE_CONTEXT) {
@@ -52,7 +56,7 @@ class Innertube {
this.sts = data.STS;
this.context.client.hl = 'en';
this.context.client.gl = 'US';
this.context.client.gl = this.config.gl || 'US';
/**
* @event Innertube#auth - Fired when signing in to an account.
@@ -65,26 +69,13 @@ class Innertube {
this.#player = new Player(this);
await this.#player.init();
if (this.logged_in && this.cookie.length) {
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
if (this.logged_in && this.config.cookie) {
this.auth_apisid = Utils.getStringBetweenStrings(this.config.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.request = new Request(this);
this.#initMethods();
} else {
this.#retry_count += 1;
@@ -107,7 +98,7 @@ class Innertube {
* Notify about activity from the channels you're subscribed to.
*
* @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),
@@ -115,7 +106,7 @@ class Innertube {
* Recommended content notifications.
*
* @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),
@@ -123,7 +114,7 @@ class Innertube {
* Notify about activity on your channel.
*
* @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),
@@ -131,7 +122,7 @@ class Innertube {
* Notify about replies to your comments.
*
* @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),
@@ -139,7 +130,7 @@ class Innertube {
* Notify when others mention your channel.
*
* @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),
@@ -147,7 +138,7 @@ class Innertube {
* Notify when others share your content on their channels.
*
* @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)
},
@@ -156,7 +147,7 @@ class Innertube {
* If set to true, your subscriptions won't be visible to others.
*
* @param {boolean} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'account_privacy', new_value),
@@ -164,7 +155,7 @@ class Innertube {
* If set to true, saved playlists won't appear on your channel.
*
* @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)
}
@@ -176,7 +167,7 @@ class Innertube {
* Likes a given video.
*
* @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 }),
@@ -184,7 +175,7 @@ class Innertube {
* Diskes a given video.
*
* @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 }),
@@ -192,7 +183,7 @@ class Innertube {
* Removes a like/dislike.
*
* @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 }),
@@ -201,7 +192,7 @@ class Innertube {
*
* @param {string} video_id
* @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 }),
@@ -209,7 +200,7 @@ class Innertube {
* Subscribes to a given channel.
*
* @param {string} channel_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
subscribe: (channel_id) => Actions.engage(this, 'subscription/subscribe', { channel_id }),
@@ -217,7 +208,7 @@ class Innertube {
* Unsubscribes from a given channel.
*
* @param {string} channel_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
unsubscribe: (channel_id) => Actions.engage(this, 'subscription/unsubscribe', { channel_id }),
@@ -227,9 +218,9 @@ class Innertube {
*
* @param {string} channel_id
* @param {string} type PERSONALIZED | ALL | NONE
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
changeNotificationPreferences: (channel_id, type) => Actions.notifications(this, 'modify_channel_preference', { channel_id, pref: type || 'NONE' }),
setNotificationPreferences: (channel_id, type) => Actions.notifications(this, 'modify_channel_preference', { channel_id, pref: type || 'NONE' }),
};
this.playlist = {
@@ -238,7 +229,7 @@ class Innertube {
*
* @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; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
create: (title, video_id) => Actions.engage(this, 'playlist/create', { title, video_id }),
@@ -246,15 +237,16 @@ class Innertube {
* Deletes a given playlist.
*
* @param {string} playlist_id
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
delete: (playlist_id) => Actions.engage(this, 'playlist/delete', { 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; }>}
*/
addVideos: (playlist_id, video_ids) => Actions.engage(this, 'browse/edit_playlist', { action: 'ACTION_ADD_VIDEO', playlist_id, video_ids })
};
@@ -266,7 +258,7 @@ class Innertube {
* @param {string} setting_id
* @param {string} type
* @param {string} new_value
* @returns {Promise<{ success: boolean; status_code: string; }>}
* @returns {Promise.<{ success: boolean; status_code: string; }>}
*/
async #setSetting(setting_id, type, new_value) {
const response = await Actions.browse(this, type);
@@ -320,14 +312,6 @@ class Innertube {
this.access_token = this.#oauth.getAccessToken();
this.refresh_token = this.#oauth.getRefreshToken();
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 });
}
/**
@@ -342,8 +326,8 @@ class Innertube {
}
/**
* 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');
@@ -384,7 +368,7 @@ class Innertube {
}
/**
* Gets search suggestions.
* Retrieves search suggestions.
*
* @param {string} input - The search query.
* @param {object} [options] - Search options.
@@ -405,9 +389,9 @@ class Innertube {
}
/**
* 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 }>}
*/
async getDetails(video_id) {
@@ -421,23 +405,46 @@ class Innertube {
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.getComments = (sort_by) => this.getComments(video_id, sort_by);
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' });
details.setNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
return details;
}
/**
* 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 getComments(video_id, sort_by) {
const payload = Proto.encodeCommentsSectionParams(video_id, {
sort_by: sort_by || 'TOP_COMMENTS'
});
const response = await Actions.next(this, { continuation_token: payload });
if (!response.success) throw new Utils.InnertubeError('Could not retrieve comments', response);
const comments = new Parser(this, response.data, {
video_id,
client: 'YOUTUBE',
data_type: 'COMMENTS'
}).parse();
return comments;
}
/**
* Gets info about a given channel. (WIP)
* Retrieves contents for a given channel. (WIP)
*
* @param {string} id - The id of the channel.
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
@@ -453,142 +460,9 @@ class Innertube {
return channel_info;
}
/**
* 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 lyrics_tab = continuation.data.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer
.watchNextTabbedResultsRenderer.tabs.find((obj) => obj.tabRenderer.title == 'Lyrics');
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;
}
/**
* Parses a given playlist.
*
* @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: [] }>}
*/
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'
}).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;
}
/**
* Returns your watch history.
* Retrieves your watch history.
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
*/
async getHistory() {
@@ -604,7 +478,7 @@ class Innertube {
}
/**
* 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() {
@@ -619,6 +493,12 @@ class Innertube {
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');
if (!response.success) throw new Utils.InnertubeError('Could not retrieve trending content', response);
@@ -632,7 +512,7 @@ class Innertube {
}
/**
* Returns your subscription feed.
* Retrieves your subscriptions feed.
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
*/
async getSubscriptionsFeed() {
@@ -664,7 +544,7 @@ class Innertube {
}
/**
* Returns unseen notifications count.
* Retrieves unseen notifications count.
* @returns {Promise.<number>} unseen notifications count.
*/
async getUnseenNotificationsCount() {
@@ -673,6 +553,47 @@ class Innertube {
return response.data.unseenCount;
}
/**
* Retrieves 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 lyrics_tab = Utils.findNode(continuation, 'contents', 'Lyrics', 8, false);
const response = await Actions.browse(this, 'lyrics', { ytmusic: true, browse_id: lyrics_tab.endpoint?.browseEndpoint.browseId });
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;
}
/**
* Parses a given playlist.
*
* @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: [] }>}
*/
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'
}).parse();
return playlist;
}
/**
* Internal method to process and filter formats.
*
@@ -691,7 +612,7 @@ class Innertube {
format.url = format.url || format.signatureCipher || format.cipher;
if (format.signatureCipher || format.cipher) {
format.url = new SigDecipher(format.url, this.#player).decipher();
format.url = new Signature(format.url, this.#player).decipher();
}
const url_components = new URL(format.url);
@@ -746,7 +667,7 @@ class Innertube {
* An alternative to {@link download}.
* Returns deciphered streaming data.
*
* @param {string} id - The id of the video.
* @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
@@ -768,7 +689,7 @@ class Innertube {
/**
* Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}.
*
* @param {string} id - The id of the video.
* @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
@@ -915,4 +836,4 @@ class Innertube {
}
}
module.exports = Innertube;
module.exports = Innertube;

View File

@@ -9,9 +9,9 @@ const Constants = require('../utils/Constants');
/**
* Performs direct interactions on YouTube.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} engagement_type - Type of engagement.
* @param {object} args - Engagement arguments.
* @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 = {}) {
@@ -41,8 +41,8 @@ async function engage(session, engagement_type, args = {}) {
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),
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;
@@ -64,7 +64,7 @@ async function engage(session, engagement_type, args = {}) {
throw new Utils.InnertubeError('Invalid action', engagement_type);
}
const response = await session.YTRequester.post(`/${engagement_type}`, JSON.stringify(data)).catch((error) => error);
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 };
return {
@@ -76,9 +76,9 @@ async function engage(session, engagement_type, args = {}) {
/**
* Accesses YouTube's various sections.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action argumenets.
* @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 = {}) {
@@ -129,8 +129,7 @@ async function browse(session, action, args = {}) {
throw new Utils.InnertubeError('Invalid action', action);
}
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
const response = await requester.post('/browse', JSON.stringify(data)).catch((error) => error);
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 {
@@ -140,13 +139,45 @@ async function browse(session, action, args = {}) {
};
}
/**
* 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
};
}
/**
* Account settings endpoints.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action argumenets.
* @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 = {}) {
@@ -166,7 +197,7 @@ async function account(session, action, args = {}) {
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((error) => error);
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 {
@@ -179,10 +210,10 @@ async function account(session, action, args = {}) {
/**
* 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.
* @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) {
@@ -202,7 +233,7 @@ async function music(session, action, args) {
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await session.YTMRequester.post(`/music/${action}`, JSON.stringify(data)).catch((error) => error);
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 {
@@ -215,7 +246,7 @@ async function music(session, action, args) {
/**
* Searches a given query on YouTube/YTMusic.
*
* @param {Innertube} session - A valid Innertube session.
* @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 }>}
@@ -245,8 +276,7 @@ async function search(session, client, args = {}) {
throw new Utils.InnertubeError('Invalid client', client);
}
const requester = client == 'YOUTUBE' && session.YTRequester || session.YTMRequester;
const response = await requester.post('/search', JSON.stringify(data)).catch((error) => error);
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 {
@@ -259,9 +289,9 @@ async function search(session, client, args = {}) {
/**
* Interacts with YouTube's notification system.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action arguments.
* @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 = {}) {
@@ -286,7 +316,7 @@ async function notifications(session, action, args = {}) {
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await session.YTRequester.post(`/notification/${action}`, JSON.stringify(data)).catch((err) => err);
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 };
@@ -300,9 +330,9 @@ async function notifications(session, action, args = {}) {
/**
* Interacts with YouTube's livechat system.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action - Type of action.
* @param {object} args - Action arguments.
* @param {Innertube} session
* @param {string} action
* @param {object} args
* @returns {Promise.<{ success: boolean; data: object; message?: string }>}
*/
async function livechat(session, action, args = {}) {
@@ -315,7 +345,7 @@ async function livechat(session, action, args = {}) {
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.clientMessageId = Uuid.v4();
data.richMessage = {
textSegments: [{ text: args.text }]
}
@@ -336,7 +366,7 @@ async function livechat(session, action, args = {}) {
throw new Utils.InnertubeError('Invalid action', action);
}
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((err) => err);
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 };
@@ -345,8 +375,8 @@ async function livechat(session, action, args = {}) {
/**
* Requests continuation for previously performed actions.
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Continuation arguments.
* @param {Innertube} session
* @param {object} args
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function next(session, args = {}) {
@@ -374,14 +404,8 @@ async function next(session, args = {}) {
}
}
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
};
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,
@@ -393,12 +417,12 @@ async function next(session, args = {}) {
/**
* Retrieves video data.
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Request arguments.
* @param {Innertube} session
* @param {object} args
* @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);
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;
}
@@ -406,8 +430,8 @@ async function getVideoInfo(session, args = {}) {
/**
* Gets search suggestions.
*
* @param {Innertube} session - A valid innertube session
* @param {string} query - Search query
* @param {Innertube} session
* @param {string} query
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function getSearchSuggestions(session, client, input) {
@@ -417,7 +441,7 @@ async function getSearchSuggestions(session, client, input) {
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);
Constants.DEFAULT_HEADERS(session.config)).catch((error) => error);
return {
success: !(response instanceof Error),
@@ -434,4 +458,4 @@ async function getSearchSuggestions(session, client, input) {
return response;
}
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, next, getSearchSuggestions };
module.exports = { engage, browse, account, flag, music, search, notifications, livechat, getVideoInfo, next, getSearchSuggestions };

View File

@@ -197,7 +197,7 @@ class OAuth {
const url_body = Constants.OAUTH.REGEX.AUTH_SCRIPT.exec(yttv_response.data)[1];
const script_url = `${Constants.URLS.YT_BASE}/${url_body}`;
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error);
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS()).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`);
const client_identity = response.data.replace(/\n/g, '').match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);

View File

@@ -2,7 +2,7 @@
const QueryString = require('querystring');
class SigDecipher {
class Signature {
constructor(url, player) {
this.url = url;
this.player = player;
@@ -72,4 +72,4 @@ class SigDecipher {
}
}
module.exports = SigDecipher;
module.exports = Signature;

View File

@@ -5,6 +5,7 @@ const Actions = require('../core/Actions');
const Constants = require('../utils/Constants');
const YTDataItems = require('./youtube');
const YTMusicDataItems = require('./ytmusic');
const Proto = require('../proto');
class Parser {
constructor(session, data, args = {}) {
@@ -29,6 +30,7 @@ class Parser {
HOMEFEED: () => this.#processHomeFeed(),
TRENDING: () => this.#processTrending(),
HISTORY: () => this.#processHistory(),
COMMENTS: () => this.#processComments(),
VIDEO_INFO: () => this.#processVideoInfo(),
NOTIFICATIONS: () => this.#processNotifications(),
SEARCH_SUGGESTIONS: () => this.#processSearchSuggestions(),
@@ -101,7 +103,7 @@ class Parser {
const section_title = section.title.runs[0].text;
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),
['Videos']: () => YTMusicDataItems.VideoResultItem.parse(section.contents),
['Featured playlists']: () => YTMusicDataItems.PlaylistResultItem.parse(section.contents),
@@ -268,6 +270,130 @@ class Parser {
return processed_data;
}
#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 = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id }),
comment.dislike = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id }),
comment.reply = (text) => Actions.engage(this.session, '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 Actions.flag(this.session, 'flag/get_form', { params: payload.params });
const action = Utils.findNode(form, 'actions', 'flagAction', 13, false);
const flag = await Actions.flag(this.session, '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 Actions.next(this.session, { continuation_token: payload });
return parseComments(next.data);
};
return comment;
}
}).filter((c) => c);
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 Actions.next(this.session, { continuation_token: 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 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);
}
#processChannel() {
const tabs = this.data.contents.twoColumnBrowseResultsRenderer.tabs;
const metadata = this.data.metadata;
@@ -342,7 +468,6 @@ class Parser {
#processTrending() {
const tabs = Utils.findNode(this.data, 'contents', 'tabRenderer', 4, false);
const categories = {};
const trending = tabs.map((tab) => {
@@ -418,68 +543,6 @@ class Parser {
return parseItems(contents);
}
#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;

View File

@@ -9,5 +9,6 @@ const GridVideoItem = require('./others/GridVideoItem');
const GridPlaylistItem = require('./others/GridPlaylistItem');
const ChannelMetadata = require('./others/ChannelMetadata');
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 };

View File

@@ -0,0 +1,37 @@
'use strict';
const Constants = require('../../../utils/Constants');
class CommentThread {
static parseItem(item) {
if (item.commentThreadRenderer || item.commentRenderer) {
const comment = item?.commentThreadRenderer?.comment || item;
const replies = item?.commentThreadRenderer?.replies;
const like_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.likeButton;
const dislike_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.dislikeButton;
return {
text: comment.commentRenderer.contentText.runs.map((run) => run.text).join(''),
author: {
name: comment.commentRenderer.authorText.simpleText,
thumbnails: comment.commentRenderer.authorThumbnail.thumbnails,
channel_id: comment.commentRenderer.authorEndpoint.browseEndpoint.browseId,
channel_url: Constants.URLS.YT_BASE + comment.commentRenderer.authorEndpoint.browseEndpoint.canonicalBaseUrl
},
metadata: {
published: comment.commentRenderer.publishedTimeText.runs[0].text,
is_reply: !!item.commentRenderer,
is_liked: like_btn.toggleButtonRenderer.isToggled,
is_disliked: dislike_btn.toggleButtonRenderer.isToggled,
is_pinned: comment.commentRenderer.pinnedCommentBadge && true || false,
is_channel_owner: comment.commentRenderer.authorIsChannelOwner,
like_count: parseInt(like_btn?.toggleButtonRenderer?.accessibilityData?.accessibilityData.label.replace(/\D/g, '')),
reply_count: comment.commentRenderer.replyCount || 0,
id: comment.commentRenderer.commentId,
}
}
}
}
}
module.exports = CommentThread;

View File

@@ -7,8 +7,8 @@ class PlaylistResultItem {
static parseItem(item) {
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;
return {
id: watch_playlist_endpoint?.playlistId,

View File

@@ -1,133 +1,109 @@
'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 {
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(search_filter_buff).toString('base64'));
}
/**
* 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`));
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,
});
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(buf).toString('base64'));
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
}
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'));
}
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'));
}
static encodeCommentParams(video_id) {
const buf = messages.CreateCommentParams.encode({
video_id, params: { index: 0 },
number: 7
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
static encodeCommentReplyParams(comment_id, video_id) {
const buf = messages.CreateCommentReplyParams.encode({
video_id, comment_id,
params: { unk_num: 0 },
unk_num: 7
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
static encodeCommentActionParams(type, comment_id, video_id) {
const buf = messages.PeformCommentActionParams.encode({
type, comment_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,
});
return encodeURIComponent(Buffer.from(buf).toString('base64'));
}
static encodeNotificationPref(channel_id, index) {
const buf = messages.NotificationPreferences.encode({
channel_id, pref_id: { index },
number_0: 0, number_1: 4
});
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;

1751
lib/proto/messages.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,66 +2,111 @@ syntax = "proto2";
package proto;
message NotificationPreferences {
string channel_id = 1;
required string channel_id = 1;
message Preference {
int32 index = 1;
required int32 index = 1;
}
Preference pref_id = 2;
int32 number_0 = 3;
int32 number_1 = 4;
optional int32 number_0 = 3;
optional int32 number_1 = 4;
}
message LiveMessageParams {
message Params {
message Ids {
string channel_id = 1;
string video_id = 2;
required string channel_id = 1;
required string video_id = 2;
}
Ids ids = 5;
}
Params params = 1;
int32 number_0 = 2;
int32 number_1 = 3;
optional int32 number_0 = 2;
optional int32 number_1 = 3;
}
message GetCommentsSectionParams {
message Context {
string video_id = 2;
}
Context ctx = 2;
required int32 unk_param = 3;
message Params {
optional string unk_token = 1;
message Options {
required string video_id = 4;
required int32 sort_by = 6;
required int32 type = 15;
}
message RepliesOptions {
required string comment_id = 2;
message UnkOpts {
required int32 unk_param = 1;
}
UnkOpts unkopts = 4;
optional string channel_id = 5;
required string video_id = 6;
required int32 unk_param_1 = 8;
required int32 unk_param_2 = 9;
}
optional Options opts = 4;
optional RepliesOptions replies_opts = 3;
optional int32 page = 5;
required string target = 8;
}
Params params = 6;
}
message CreateCommentParams {
string video_id = 2;
required string video_id = 2;
message Params {
int32 index = 1;
required int32 index = 1;
}
Params params = 5;
int32 number = 10;
required int32 number = 10;
}
message CreateCommentReplyParams {
string video_id = 2;
string comment_id = 4;
required string video_id = 2;
required string comment_id = 4;
message UnknownParams {
int32 unk_num = 1;
required int32 unk_num = 1;
}
UnknownParams params = 5;
int32 unk_num = 10;
optional int32 unk_num = 10;
}
message PeformCommentActionParams {
int32 type = 1;
int32 unk_num = 2;
required int32 type = 1;
optional int32 unk_num = 2;
string comment_id = 3;
string video_id = 5;
required string comment_id = 3;
required string video_id = 5;
int32 unk_num_1 = 6;
int32 unk_num_2 = 7;
optional int32 unk_num_1 = 6;
optional int32 unk_num_2 = 7;
string unk_num_3 = 9;
optional string unk_num_3 = 9;
int32 unk_num_4 = 10;
int32 unk_num_5 = 21;
optional int32 unk_num_4 = 10;
optional int32 unk_num_5 = 21;
string channel_id = 23;
int32 unk_num_6 = 30;
optional string channel_id = 23;
optional int32 unk_num_6 = 30;
}
message SearchFilter {

View File

@@ -30,14 +30,14 @@ module.exports = {
CLIENT_IDENTITY: /.+?={};var .+?={clientId:\"(?<id>.+?)\",.+?:\"(?<secret>.+?)\"},/
}
},
DEFAULT_HEADERS: (session) => {
DEFAULT_HEADERS: (config) => {
return {
headers: {
'Cookie': session.cookie,
'Cookie': config?.cookie || '',
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
'Referer': 'https://www.google.com/',
'Accept': 'text/html',
'Accept-Language': 'en-US,en',
'Accept-Language': `en-${config?.gl || 'US'}`,
'Accept-Encoding': 'gzip'
}
};
@@ -50,31 +50,9 @@ module.exports = {
'Referer': 'https://www.youtube.com',
'DNT': '?1'
},
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 {

53
lib/utils/Request.js Normal file
View File

@@ -0,0 +1,53 @@
'use strict';
const Axios = require('axios');
const Utils = require('./Utils');
const Constants = require('./Constants');
class Request {
constructor (session) {
this.session = session;
this.instance = Axios.create({
baseURL: Constants.URLS.YT_BASE_API + session.version,
headers: Constants.INNERTUBE_HEADERS_BASE,
params: { key: session.key },
timeout: 15000
});
this.#setupInterceptor();
return this.instance;
}
#setupInterceptor() {
this.instance.interceptors.request.use((config) => {
const is_ytmusic = config.data.includes(Constants.URLS.YT_MUSIC);
config.headers['accept-language'] = `en-${this.session.config.gl || 'US'}`;
config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || ''
config.headers['x-youtube-client-version'] = this.session.context.client.clientVersion;
config.headers['x-origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
config.headers['origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
is_ytmusic && (config.baseURL = Constants.URLS.YT_MUSIC_BASE_API + this.session.version);
if (this.session.logged_in) {
const cookie = this.session.config.cookie;
const token = cookie
&& this.session.auth_apisid
|| this.session.access_token;
config.headers.cookie = cookie || '';
config.headers.authorization = cookie && token || `Bearer ${token}`;
!cookie && (delete config.params.key);
}
return config;
}, (error) => Promise.reject(error));
}
}
module.exports = Request;

View File

@@ -1,6 +1,5 @@
'use strict';
const Fs = require('fs');
const Crypto = require('crypto');
const UserAgent = require('user-agents');
const Flatten = require('flat');

90
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "youtubei.js",
"version": "1.4.1",
"version": "1.4.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "1.4.1",
"version": "1.4.2",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
"flat": "^5.0.2",
"protons": "^2.0.3",
"protocol-buffers-encodings": "^1.1.1",
"user-agents": "^1.0.778",
"uuid": "^8.3.2"
},
@@ -91,25 +91,13 @@
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"node_modules/multiformats": {
"version": "9.6.4",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz",
"integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg=="
},
"node_modules/protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
"node_modules/protons": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
"node_modules/protocol-buffers-encodings": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.1.1.tgz",
"integrity": "sha512-5aFshI9SbhtcMiDiZZu3g2tMlZeS5lhni//AGJ7V34PQLU5JA91Cva7TIs6inZhYikS3OpnUzAUuL6YtS0CyDA==",
"dependencies": {
"protocol-buffers-schema": "^3.3.1",
"signed-varint": "^2.0.1",
"uint8arrays": "^3.0.0",
"varint": "^5.0.0"
"varint": "5.0.0"
}
},
"node_modules/signed-varint": {
@@ -120,14 +108,6 @@
"varint": "~5.0.0"
}
},
"node_modules/uint8arrays": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
"dependencies": {
"multiformats": "^9.4.2"
}
},
"node_modules/underscore": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
@@ -142,9 +122,9 @@
}
},
"node_modules/user-agents": {
"version": "1.0.989",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.989.tgz",
"integrity": "sha512-HiQ6M3NWbil5gb00J3r8mITIbOzmFmWZJCPcw3l5Dm5tfjl6x/yrHNFhZKLTZJQ6K4Gz4UPmJUtN6T2XSCYJhQ==",
"version": "1.0.993",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.993.tgz",
"integrity": "sha512-15uxQ45RVVNSWLkW9V3KkHoQIp+3evKLAfJSe6WOYNLF897mn7m1LTMn4IC7n4CmviDlQJ/SKyCXEutcYo1rAQ==",
"dependencies": {
"dot-json": "^1.2.2",
"lodash.clonedeep": "^4.5.0"
@@ -159,9 +139,9 @@
}
},
"node_modules/varint": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
"integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
}
},
"dependencies": {
@@ -208,25 +188,13 @@
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"multiformats": {
"version": "9.6.4",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz",
"integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg=="
},
"protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
"protons": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
"protocol-buffers-encodings": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.1.1.tgz",
"integrity": "sha512-5aFshI9SbhtcMiDiZZu3g2tMlZeS5lhni//AGJ7V34PQLU5JA91Cva7TIs6inZhYikS3OpnUzAUuL6YtS0CyDA==",
"requires": {
"protocol-buffers-schema": "^3.3.1",
"signed-varint": "^2.0.1",
"uint8arrays": "^3.0.0",
"varint": "^5.0.0"
"varint": "5.0.0"
}
},
"signed-varint": {
@@ -237,14 +205,6 @@
"varint": "~5.0.0"
}
},
"uint8arrays": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
"requires": {
"multiformats": "^9.4.2"
}
},
"underscore": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
@@ -259,9 +219,9 @@
}
},
"user-agents": {
"version": "1.0.989",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.989.tgz",
"integrity": "sha512-HiQ6M3NWbil5gb00J3r8mITIbOzmFmWZJCPcw3l5Dm5tfjl6x/yrHNFhZKLTZJQ6K4Gz4UPmJUtN6T2XSCYJhQ==",
"version": "1.0.993",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.993.tgz",
"integrity": "sha512-15uxQ45RVVNSWLkW9V3KkHoQIp+3evKLAfJSe6WOYNLF897mn7m1LTMn4IC7n4CmviDlQJ/SKyCXEutcYo1rAQ==",
"requires": {
"dot-json": "^1.2.2",
"lodash.clonedeep": "^4.5.0"
@@ -273,9 +233,9 @@
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"varint": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
"integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "1.4.1",
"version": "1.4.2",
"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!",
"main": "index.js",
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)",
@@ -22,7 +22,7 @@
"dependencies": {
"axios": "^0.21.4",
"flat": "^5.0.2",
"protons": "^2.0.3",
"protocol-buffers-encodings": "^1.1.1",
"user-agents": "^1.0.778",
"uuid": "^8.3.2"
},
@@ -43,6 +43,7 @@
"youtube-downloader",
"innertube",
"innertubeapi",
"unofficial",
"downloader",
"livechat",
"dislike",

View File

@@ -3,7 +3,7 @@
const Fs = require('fs');
const Innertube = require('..');
const NToken = require('../lib/deciphers/NToken');
const SigDecipher = require('../lib/deciphers/Sig');
const Signature = require('../lib/deciphers/Signature');
const Constants = require('./constants');
let failed_tests_count = 0;
@@ -24,9 +24,9 @@ async function performTests() {
const ytsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YOUTUBE' }).catch((error) => error);
assert(!(ytsearch_suggestions instanceof Error), `should retrieve YouTube search suggestions`, ytsearch_suggestions);
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' });
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search 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 details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details);
@@ -50,8 +50,8 @@ async function performTests() {
const n_token = new NToken(Constants.n_scramble_sc, Constants.original_ntoken).transform();
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token);
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, { sig_decipher_sc: Constants.sig_decipher_sc }).decipher();
assert(transformed_url == Constants.expected_url, `should decipher signature`, transformed_url);
if (failed_tests_count > 0)
throw new Error(`${failed_tests_count} tests have failed`);
@@ -70,7 +70,7 @@ function downloadVideo(id, youtube) {
function assert(outcome, description, data) {
const pass_fail = outcome ? 'pass' : 'fail';
console.info(pass_fail, ':', description);
!outcome && (failed_tests_count += 1);
!outcome && console.error('Error: ', data);
@@ -78,4 +78,4 @@ function assert(outcome, description, data) {
return outcome;
}
performTests();
performTests();

31
typings/index.d.ts vendored
View File

@@ -23,6 +23,7 @@ interface YouTubeSearch {
corrected_query: string;
estimated_results: number;
videos: any[];
getContinuation: () => Promise<object>;
}
interface YouTubeMusicSearch {
@@ -42,6 +43,8 @@ type SearchResults = YouTubeSearch | YouTubeMusicSearch;
type ClientOption = Pick<SearchOptions, 'client'>;
type SortBy = 'TOP_COMMENTS' | 'NEWEST_FIRST';
interface Suggestion {
text: string;
bold_text: string;
@@ -55,8 +58,10 @@ interface ApiStatus {
}
interface Comments {
comments: any[];
comment_count?: string;
page_count: number,
comment_count: number;
items: any[];
getContinuation: () => Promise<object>;
}
interface Video {
@@ -72,7 +77,7 @@ interface Video {
comment: (text: string) => Promise<ApiStatus>;
getComments: () => Promise<Comments>;
getLivechat: () => any; // TODO type LiveChat
changeNotificationPreferences: () => Promise<ApiStatus>;
setNotificationPreferences: () => Promise<ApiStatus>;
}
interface Channel {
@@ -93,16 +98,12 @@ interface PlayList {
year?: string;
}
interface CommentData {
token: string;
channel_id: string;
}
interface History {
items: {
date: string;
videos: any[];
}[];
getContinuation: () => Promise<object>;
}
interface SubscriptionFeed {
@@ -110,6 +111,7 @@ interface SubscriptionFeed {
date: string;
videos: any[];
}[];
getContinuation: () => Promise<object>;
}
interface HomeFeed {
@@ -120,6 +122,7 @@ interface HomeFeed {
channel: string;
metadata: Record<string, any>;
}[];
getContinuation: () => Promise<object>;
}
interface Trending {
@@ -145,6 +148,7 @@ interface Notifications {
read: boolean;
notification_id: string;
}[];
getContinuation: () => Promise<object>;
}
interface StreamingData {
@@ -157,19 +161,24 @@ interface StreamingOptions {
format?: string;
}
interface Config {
gl?: string;
cookie?: string;
}
export default class Innertube {
constructor(cookie?: string)
constructor(auth_info?: Config)
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 getSearchSuggestions(query: string, 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 getComments(video_id: string, sort_by?: SortBy): Promise<Comments>;
public getHistory(): Promise<History>;
public getHomeFeed(): Promise<HomeFeed>;
public getTrending(): Promise<Trending>;