mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-17 11:32:27 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db41fa40d2 | ||
|
|
02ece1ddda | ||
|
|
b175e02f6d | ||
|
|
d3394f846a | ||
|
|
07b73ab78d | ||
|
|
d743b5a088 | ||
|
|
bb206c044c | ||
|
|
d48065405d | ||
|
|
dbc8b62ba2 |
34
README.md
34
README.md
@@ -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: '...' });
|
||||
//...
|
||||
}
|
||||
|
||||
|
||||
333
lib/Innertube.js
333
lib/Innertube.js
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
37
lib/parser/youtube/others/CommentThread.js
Normal file
37
lib/parser/youtube/others/CommentThread.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
1751
lib/proto/messages.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
53
lib/utils/Request.js
Normal 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;
|
||||
@@ -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
90
package-lock.json
generated
@@ -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="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
31
typings/index.d.ts
vendored
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user