mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 10:02:16 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5090c572d5 | ||
|
|
c9c72d0f31 | ||
|
|
7635f49191 | ||
|
|
c932e65dad | ||
|
|
23717aab11 | ||
|
|
85df28a7fb | ||
|
|
9f4970b3ee | ||
|
|
82bbc715ff | ||
|
|
3ec111212c | ||
|
|
7ca4b2bb45 | ||
|
|
8d411f25c8 | ||
|
|
80fe969917 | ||
|
|
13c94fbb8a | ||
|
|
60ce869054 | ||
|
|
1268ac83a6 | ||
|
|
5e588d0db5 | ||
|
|
8b37bd99b1 | ||
|
|
08741de831 | ||
|
|
574a595a01 | ||
|
|
16928ee71b | ||
|
|
de6283080b | ||
|
|
23ab8bca4d | ||
|
|
068b86b410 | ||
|
|
0b001c0956 | ||
|
|
4c14662d42 | ||
|
|
f1a9d5d77b | ||
|
|
398cd8728d | ||
|
|
459c30528e | ||
|
|
6e1e96610c |
132
README.md
132
README.md
@@ -49,17 +49,17 @@ npm install youtubei.js
|
||||
|
||||
## Usage
|
||||
|
||||
[1. Getting Started](https://github.com/LuanRT/YouTube.js#usage)
|
||||
[1. Getting Started](#usage)
|
||||
|
||||
[2. Interactions](https://github.com/LuanRT/YouTube.js#interactions)
|
||||
[2. Interactions](#interactions)
|
||||
|
||||
[3. Live chats](https://github.com/LuanRT/YouTube.js#fetching-live-chats)
|
||||
[3. Live chats](#fetching-live-chats)
|
||||
|
||||
[4. Downloading videos](https://github.com/LuanRT/YouTube.js#downloading-videos)
|
||||
[4. Downloading videos](#downloading-videos)
|
||||
|
||||
[5. Signing-in](https://github.com/LuanRT/YouTube.js#signing-in)
|
||||
[5. Signing-in](#signing-in)
|
||||
|
||||
[6. Disclaimer](https://github.com/LuanRT/YouTube.js#disclaimer)
|
||||
[6. Disclaimer](#disclaimer)
|
||||
|
||||
First of all we're gonna start by initializing the Innertube instance.
|
||||
And to make things faster, you should do this only once and reuse the Innertube object when needed.
|
||||
@@ -90,9 +90,9 @@ const search = await youtube.search('Interstellar Main Theme', { client: 'YTMUSI
|
||||
estimated_results: number,
|
||||
videos: [
|
||||
{
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
id: string,
|
||||
url: string,
|
||||
metadata:{
|
||||
view_count: string,
|
||||
@@ -211,7 +211,7 @@ const suggestions = await youtube.getSearchSuggestions('QUERY', {
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get details about a specific video:
|
||||
Get details about a given video:
|
||||
|
||||
```js
|
||||
const video = await youtube.getDetails('VIDEO_ID');
|
||||
@@ -246,10 +246,18 @@ const video = await youtube.getDetails('VIDEO_ID');
|
||||
channel_id: string,
|
||||
channel_url: string,
|
||||
external_channel_id: string,
|
||||
allow_ratings: boolean,
|
||||
is_live_content: boolean,
|
||||
is_family_safe: boolean,
|
||||
is_unlisted: boolean,
|
||||
is_private: boolean,
|
||||
is_liked: boolean,
|
||||
is_disliked: boolean,
|
||||
is_subscribed: boolean,
|
||||
subscriber_count: string,
|
||||
current_notification_preference: string,
|
||||
likes: { count: number, short_count_text: string },
|
||||
publish_date_text: string,
|
||||
has_ypc_metadata: boolean,
|
||||
category: string,
|
||||
channel_name: string,
|
||||
@@ -266,14 +274,22 @@ const video = await youtube.getDetails('VIDEO_ID');
|
||||
Get comments:
|
||||
|
||||
```js
|
||||
const comments = await youtube.getComments('VIDEO_ID');
|
||||
const response = await youtube.getComments('VIDEO_ID');
|
||||
|
||||
// Or:
|
||||
const video = await youtube.getDetails('VIDEO_ID');
|
||||
const comments = await video.getComments();
|
||||
const response = await video.getComments();
|
||||
|
||||
// Get comments continuation:
|
||||
const continuation = await comments.getContinuation();
|
||||
// Get comment replies:
|
||||
const replies = await response.comments[0].getReplies();
|
||||
|
||||
// Like, dislike, reply (same logic for replies):
|
||||
await response.comments[0].like();
|
||||
await response.comments[0].dislike();
|
||||
await response.comments[0].reply('Nice comment!');
|
||||
|
||||
// Get comments continuation (same logic for replies):
|
||||
const continuation = await response.getContinuation();
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -299,6 +315,8 @@ const continuation = await comments.getContinuation();
|
||||
metadata:{
|
||||
published: string,
|
||||
is_liked: boolean,
|
||||
is_disliked: boolean,
|
||||
is_pinned: boolean,
|
||||
is_channel_owner: boolean,
|
||||
like_count: number,
|
||||
reply_count: number,
|
||||
@@ -327,22 +345,29 @@ const homefeed = await youtube.getHomeFeed();
|
||||
{
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
channel: string,
|
||||
metadata: {
|
||||
view_count: string,
|
||||
short_view_count_text: { simple_text: string, accessibility_label: string },
|
||||
thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
moving_thumbnail: {
|
||||
},
|
||||
moving_thumbnail: {
|
||||
url: string,
|
||||
widt: number,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
published: string,
|
||||
badges: [Array],
|
||||
owner_badges: [Array]
|
||||
},
|
||||
published: string,
|
||||
duration: {
|
||||
seconds: number,
|
||||
simple_text: string,
|
||||
accessibility_label: string
|
||||
},
|
||||
badges: string,
|
||||
owner_badges: [Array]
|
||||
}
|
||||
}
|
||||
// ...
|
||||
@@ -370,19 +395,21 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();
|
||||
channel: string,
|
||||
metadata: {
|
||||
view_count: string,
|
||||
short_view_count_text: { simple_text: string, accessibility_label: string },
|
||||
thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
moving_thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
published: string,
|
||||
badges: [Array],
|
||||
badges: string,
|
||||
owner_badges: [Array]
|
||||
}
|
||||
}
|
||||
//...
|
||||
],
|
||||
@@ -393,42 +420,44 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();
|
||||
channel: string,
|
||||
metadata: {
|
||||
view_count: string,
|
||||
short_view_count_text: { simple_text: string, accessibility_label: string },
|
||||
thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
moving_thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
published: string,
|
||||
badges: [Array],
|
||||
badges: string,
|
||||
owner_badges: [Array]
|
||||
}
|
||||
}
|
||||
//...
|
||||
],
|
||||
this_week: [
|
||||
{
|
||||
id: string,
|
||||
title: string,
|
||||
channel: string,
|
||||
metadata: {
|
||||
view_count: string,
|
||||
thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
moving_thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
},
|
||||
published: string,
|
||||
badges: [Array],
|
||||
badges: string,
|
||||
owner_badges: [Array]
|
||||
}
|
||||
}
|
||||
// ...
|
||||
]
|
||||
@@ -453,9 +482,14 @@ const history = await youtube.getHistory();
|
||||
{
|
||||
id: string,
|
||||
title: string,
|
||||
channel: string,
|
||||
description: string,
|
||||
channel: {
|
||||
name: string,
|
||||
url: string
|
||||
},
|
||||
metadata: {
|
||||
view_count: string,
|
||||
short_view_count_text: { simple_text: string, accessibility_label: string },
|
||||
thumbnail: {
|
||||
url: string,
|
||||
width: number,
|
||||
@@ -467,7 +501,11 @@ const history = await youtube.getHistory();
|
||||
height: number
|
||||
},
|
||||
published: string,
|
||||
duration: string,
|
||||
duration: {
|
||||
seconds: number,
|
||||
simple_text: string,
|
||||
accessibility_label: string
|
||||
},
|
||||
badges: string,
|
||||
owner_badges: [Array]
|
||||
}
|
||||
@@ -603,7 +641,7 @@ const playlist = await youtube.getPlaylist(search.playlists[0].id);
|
||||
### Interactions:
|
||||
---
|
||||
|
||||
The library makes it easy to interact with YouTube programatically. However, don't forget that you must be signed in to use the following features!
|
||||
The library makes it easy to interact with YouTube programmatically. However, don't forget that you must be signed in to use the following features!
|
||||
|
||||
* Subscribe/Unsubscribe:
|
||||
|
||||
@@ -627,7 +665,7 @@ await youtube.interact.comment('VIDEO_ID', 'Haha, nice video!');
|
||||
* Change notification preferences:
|
||||
```js
|
||||
// Options: ALL | NONE | PERSONALIZED
|
||||
await youtube.interact.changeNotificationPreferences('VIDEO_ID', 'ALL');
|
||||
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL');
|
||||
```
|
||||
|
||||
These methods will always return ```{ success: true, status_code: 200 }``` if successful.
|
||||
@@ -761,7 +799,7 @@ async function start() {
|
||||
const search = await youtube.search('Looking for life on Mars - documentary');
|
||||
|
||||
const stream = youtube.download(search.videos[0].id, {
|
||||
format: 'mp4', // Optional, ignored when type is set to audio and defaults to mp4, and I recommend to leave it as it is
|
||||
format: 'mp4', // Optional, defaults to mp4 and I recommend to leave it as it is unless you know what you're doing
|
||||
quality: '360p', // if a video doesn't have a specific quality it'll fall back to 360p, also ignored when type is set to audio
|
||||
type: 'videoandaudio' // can be “video”, “audio” and “videoandaudio”
|
||||
});
|
||||
@@ -942,4 +980,4 @@ All trademarks, logos and brand names are the property of their respective owner
|
||||
Should you have any questions or concerns please contact me directly via email.
|
||||
|
||||
## License
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
|
||||
341
lib/Actions.js
341
lib/Actions.js
@@ -2,54 +2,53 @@
|
||||
|
||||
const Uuid = require('uuid');
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Proto = require('./proto');
|
||||
const Constants = require('./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.
|
||||
* @returns {Promise.<object>} { success: boolean, status_code: number } | { success: boolean, status_code: number, message: string }
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} engagement_type - Type of engagement.
|
||||
* @param {object} args - Engagement arguments.
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
let data;
|
||||
|
||||
const data = { context: session.context };
|
||||
switch (engagement_type) {
|
||||
case 'like/like':
|
||||
case 'like/dislike':
|
||||
case 'like/removelike':
|
||||
data = {
|
||||
context: session.context,
|
||||
target: {
|
||||
videoId: args.video_id
|
||||
}
|
||||
};
|
||||
data.target = {
|
||||
videoId: args.video_id
|
||||
}
|
||||
break;
|
||||
case 'subscription/subscribe':
|
||||
case 'subscription/unsubscribe':
|
||||
data = {
|
||||
context: session.context,
|
||||
channelIds: [args.channel_id],
|
||||
params: engagement_type == 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA'
|
||||
};
|
||||
data.channelIds = [args.channel_id];
|
||||
data.params = engagement_type == 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
|
||||
break;
|
||||
case 'comment/create_comment':
|
||||
data = {
|
||||
context: session.context,
|
||||
commentText: args.text,
|
||||
createCommentParams: Utils.encodeCommentParams(args.video_id)
|
||||
};
|
||||
data.commentText = args.text;
|
||||
data.createCommentParams = Proto.encodeCommentParams(args.video_id);
|
||||
break;
|
||||
case 'comment/create_comment_reply':
|
||||
data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id);
|
||||
data.commentText = args.text;
|
||||
break;
|
||||
case 'comment/perform_comment_action':
|
||||
const action = ({
|
||||
like: () => Proto.encodeCommentActionParams(5, args.comment_id, args.video_id, args.channel_id),
|
||||
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id, args.channel_id),
|
||||
})[args.comment_action]();
|
||||
data.actions = [ action ];
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, id: args.video_id, data })).catch((error) => error);
|
||||
|
||||
|
||||
const response = await session.YTRequester.post(`/${engagement_type}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -64,71 +63,51 @@ async function engage(session, engagement_type, args = {}) {
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action_type - Type of action.
|
||||
* @param {object} args - Action argumenets.
|
||||
* @returns {Promise.<object>} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function browse(session, action_type, args = {}) {
|
||||
if (!session.logged_in && (action_type != 'lyrics' || action_type != 'music_playlist'))
|
||||
throw new Error('You are not signed-in');
|
||||
|
||||
let data;
|
||||
if (!session.logged_in && action_type != 'home_feed'
|
||||
&& action_type !== 'lyrics' && action_type !== 'music_playlist'
|
||||
&& action_type !== 'playlist')
|
||||
throw new Error('You are not signed-in');
|
||||
|
||||
const data = { context: session.context };
|
||||
switch (action_type) {
|
||||
case 'account_notifications':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'SPaccount_notifications'
|
||||
};
|
||||
data.browseId = 'SPaccount_notifications';
|
||||
break;
|
||||
case 'account_privacy':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'SPaccount_privacy'
|
||||
};
|
||||
data.browseId = 'SPaccount_privacy';
|
||||
break;
|
||||
case 'history':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'FEhistory'
|
||||
}
|
||||
data.browseId = 'FEhistory';
|
||||
break;
|
||||
case 'home_feed':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'FEwhat_to_watch'
|
||||
};
|
||||
data.browseId = 'FEwhat_to_watch';
|
||||
break;
|
||||
case 'subscriptions_feed':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'FEsubscriptions'
|
||||
};
|
||||
data.browseId = 'FEsubscriptions';
|
||||
break;
|
||||
case 'lyrics':
|
||||
case 'music_playlist':
|
||||
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
data = {
|
||||
context,
|
||||
browseId: args.browse_id
|
||||
}
|
||||
|
||||
data.context = context;
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
case 'channel':
|
||||
case 'playlist':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: args.browse_id
|
||||
}
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const client_domain = args.ytmusic && Constants.URLS.YT_MUSIC_URL || Constants.URLS.YT_BASE_URL;
|
||||
const response = await Axios.post(`${client_domain}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: args.ytmusic })).catch((error) => error);
|
||||
|
||||
|
||||
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
|
||||
const response = await requester.post('/browse', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -145,33 +124,25 @@ async function browse(session, action_type, args = {}) {
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action_type - Type of action.
|
||||
* @param {object} args - Action argumenets.
|
||||
* @returns {Promise.<object>} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function account(session, action_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
let data;
|
||||
|
||||
const data = {};
|
||||
switch (action_type) {
|
||||
case 'account/account_menu':
|
||||
data = { context: session.context };
|
||||
data.context = session.context;
|
||||
break;
|
||||
case 'account/set_setting':
|
||||
data = {
|
||||
context: session.context,
|
||||
newValue: {
|
||||
boolValue: args.new_value
|
||||
},
|
||||
settingItemId: args.setting_item_id
|
||||
}
|
||||
data.context = session.context;
|
||||
data.newValue = { boolValue: args.new_value };
|
||||
data.settingItemId = arts.setting_item_id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
|
||||
|
||||
const response = await session.YTRequester.post(`/${action_type}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -182,18 +153,18 @@ async function account(session, action_type, args = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses YouTube Music endpoints under /youtubei/v1/music/.
|
||||
* Accesses YouTube Music endpoints (/youtubei/v1/music/).
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action_type - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @todo Implement more actions.
|
||||
* @returns
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function music(session, action_type, args) {
|
||||
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
@@ -201,18 +172,14 @@ async function music(session, action_type, args) {
|
||||
|
||||
switch (action_type) {
|
||||
case 'get_search_suggestions':
|
||||
data = {
|
||||
context,
|
||||
input: args.input || ''
|
||||
};
|
||||
data.context = context;
|
||||
data.input = args.input || '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_MUSIC_URL}/youtubei/v1/music/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: true })).catch((error) => error);
|
||||
|
||||
|
||||
const response = await session.YTMRequester.post(`/music/${action_type}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -223,45 +190,38 @@ async function music(session, action_type, args) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs searches on YouTube.
|
||||
* Searches a given query on YouTube/YTMusic.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} client - YouTube client: YOUTUBE | YTMUSIC
|
||||
* @param {object} args - Search arguments.
|
||||
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function search(session, client, args = {}) {
|
||||
if (!args.query) throw new Error('No query was provided');
|
||||
|
||||
let data;
|
||||
|
||||
const data = { context: session.context };
|
||||
switch (client) {
|
||||
case 'YOUTUBE':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order),
|
||||
query: args.query
|
||||
};
|
||||
data.params = Proto.encodeSearchFilter(args.options.period, args.options.duration, args.options.order);
|
||||
data.query = args.query;
|
||||
break;
|
||||
case 'YTMUSIC':
|
||||
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
data = {
|
||||
context: context,
|
||||
query: args.query
|
||||
};
|
||||
|
||||
data.context = context;
|
||||
data.query = args.query;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${client === 'YOUTUBE' && Constants.URLS.YT_BASE_URL || Constants.URLS.YT_MUSIC_URL}/youtubei/v1/search${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: client === 'YTMUSIC' })).catch((error) => error);
|
||||
|
||||
|
||||
const requester = client == 'YOUTUBE' && session.YTRequester || session.YTMRequester;
|
||||
const response = await requester.post('/search', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -277,38 +237,29 @@ async function search(session, client, args = {}) {
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action_type - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function notifications(session, action_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
let data;
|
||||
|
||||
const data = {};
|
||||
switch (action_type) {
|
||||
case 'modify_channel_preference':
|
||||
let pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()])
|
||||
};
|
||||
const pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
|
||||
data.context = session.context;
|
||||
data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]);
|
||||
break;
|
||||
case 'get_notification_menu':
|
||||
data = {
|
||||
context: session.context,
|
||||
notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'
|
||||
};
|
||||
data.context = session.context;
|
||||
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
|
||||
break;
|
||||
case 'get_unseen_count':
|
||||
data = {
|
||||
context: session.context
|
||||
};
|
||||
data.context = session.context;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
|
||||
|
||||
const response = await session.YTRequester.post(`/notification/${action_type}`, JSON.stringify(data)).catch((err) => err);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
|
||||
@@ -325,85 +276,52 @@ async function notifications(session, action_type, args = {}) {
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action_type - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; data: object; message?: string }>}
|
||||
*/
|
||||
async function livechat(session, action_type, args = {}) {
|
||||
let data;
|
||||
|
||||
const data = {};
|
||||
switch (action_type) {
|
||||
case 'live_chat/get_live_chat':
|
||||
data = {
|
||||
context: session.context,
|
||||
continuation: args.ctoken
|
||||
};
|
||||
data.context = session.context;
|
||||
data.continuation = args.ctoken;
|
||||
break;
|
||||
case 'live_chat/send_message':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeMessageParams(args.channel_id, args.video_id),
|
||||
clientMessageId: `ytjs-${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text: args.text }]
|
||||
}
|
||||
};
|
||||
data.context = session.context;
|
||||
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
|
||||
data.clientMessageId = `ytjs-${Uuid.v4()}`;
|
||||
data.richMessage = {
|
||||
textSegments: [{ text: args.text }]
|
||||
}
|
||||
break;
|
||||
case 'live_chat/get_item_context_menu':
|
||||
data = {
|
||||
context: session.context
|
||||
};
|
||||
data.context = session.context;
|
||||
break;
|
||||
case 'live_chat/moderate':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: args.cmd_params
|
||||
};
|
||||
data.context = session.context;
|
||||
data.params = args.cmd_params;
|
||||
break;
|
||||
case 'updated_metadata':
|
||||
data = {
|
||||
context: session.context,
|
||||
videoId: args.video_id
|
||||
};
|
||||
data.context = session.context;
|
||||
data.videoId = args.video_id;
|
||||
args.continuation && (data.continuation = args.continuation);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, params: args.params })).catch((error) => error);
|
||||
|
||||
|
||||
const response = await session.YTRequester.post(`/${action_type}`, JSON.stringify(data)).catch((err) => err);
|
||||
if (response instanceof Error) return { success: false, message: response.message };
|
||||
|
||||
|
||||
return { success: true, data: response.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets detailed data for a video.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {object} args - Request arguments.
|
||||
* @returns {Promise.<object>} - Video data.
|
||||
*/
|
||||
async function getVideoInfo(session, args = {}) {
|
||||
let response;
|
||||
|
||||
!args.desktop &&
|
||||
(response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${args.id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: false })).catch((error) => error)) ||
|
||||
(response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context)), Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: true })).catch((error) => error));
|
||||
|
||||
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests continuation for previously performed actions.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {object} args - Continuation arguments.
|
||||
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function getContinuation(session, args = {}) {
|
||||
async function next(session, args = {}) {
|
||||
let data = { context: session.context };
|
||||
args.continuation_token && (data.continuation = args.continuation_token);
|
||||
|
||||
@@ -412,7 +330,7 @@ async function getContinuation(session, args = {}) {
|
||||
if (args.ytmusic) {
|
||||
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
@@ -423,19 +341,19 @@ async function getContinuation(session, args = {}) {
|
||||
data.racyCheckOk = true;
|
||||
data.contentCheckOk = false;
|
||||
data.autonavState = 'STATE_NONE';
|
||||
data.playbackContext = {
|
||||
vis: 0,
|
||||
lactMilliseconds: '-1'
|
||||
}
|
||||
data.playbackContext = { vis: 0, lactMilliseconds: '-1' };
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
const client_domain = args.ytmusic && Constants.URLS.YT_MUSIC_URL || Constants.URLS.YT_BASE_URL;
|
||||
const response = await Axios.post(`${client_domain}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: args.ytmusic })).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
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,
|
||||
message: response.message
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -444,18 +362,35 @@ async function getContinuation(session, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video data.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {object} args - Request arguments.
|
||||
* @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);
|
||||
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets search suggestions.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} query
|
||||
* @returns
|
||||
* @param {Innertube} session - A valid innertube session
|
||||
* @param {string} query - Search query
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function getYTSearchSuggestions(session, query) {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${query}`,
|
||||
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${encodeURIComponent(query)}`,
|
||||
Constants.DEFAULT_HEADERS(session)).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
if (response instanceof Error) return {
|
||||
success: false,
|
||||
status_code: response.status,
|
||||
message: response.message
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -464,4 +399,4 @@ async function getYTSearchSuggestions(session, query) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, getContinuation, getYTSearchSuggestions };
|
||||
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, next, getYTSearchSuggestions };
|
||||
|
||||
@@ -4,11 +4,11 @@ const Utils = require('./Utils');
|
||||
|
||||
module.exports = {
|
||||
URLS: {
|
||||
YT_BASE_URL: 'https://www.youtube.com',
|
||||
YT_MUSIC_URL: 'https://music.youtube.com',
|
||||
YT_MOBILE_URL: 'https://m.youtube.com',
|
||||
YT_WATCH_PAGE: 'https://m.youtube.com/watch',
|
||||
YT_SUGGESTIONS: 'https://suggestqueries.google.com/complete/'
|
||||
YT_BASE: 'https://www.youtube.com',
|
||||
YT_BASE_API: 'https://www.youtube.com/youtubei/',
|
||||
YT_SUGGESTIONS: 'https://suggestqueries.google.com/complete/',
|
||||
YT_MUSIC: 'https://music.youtube.com',
|
||||
YT_MUSIC_BASE_API: 'https://music.youtube.com/youtubei/'
|
||||
},
|
||||
OAUTH: {
|
||||
SCOPE: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
|
||||
@@ -33,8 +33,7 @@ module.exports = {
|
||||
'Referer': 'https://www.google.com/',
|
||||
'Accept': 'text/html',
|
||||
'Accept-Language': 'en-US,en',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'Upgrade-Insecure-Requests': 1
|
||||
'Accept-Encoding': 'gzip'
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -46,36 +45,30 @@ module.exports = {
|
||||
'Referer': 'https://www.youtube.com',
|
||||
'DNT': '?1'
|
||||
},
|
||||
INNERTUBE_REQOPTS: (info) => {
|
||||
info.desktop === undefined && (info.desktop = true);
|
||||
const origin = info.ytmusic && 'https://music.youtube.com' ||
|
||||
info.desktop && 'https://www.youtube.com' || 'https://m.youtube.com';
|
||||
INNERTUBE_HEADERS: (info) => {
|
||||
const origin = info.ytmusic && 'https://music.youtube.com' || 'https://www.youtube.com';
|
||||
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'user-agent': Utils.getRandomUserAgent(info.desktop ? 'desktop' : 'mobile').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': info.desktop ? 1 : 2,
|
||||
'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 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
|
||||
};
|
||||
|
||||
info.id && (req_opts.headers.referer = (info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com') + '/watch?v=' + info.id);
|
||||
|
||||
if (info.session.logged_in && info.desktop) {
|
||||
req_opts.headers.Cookie = info.session.cookie;
|
||||
req_opts.headers.authorization = info.session.cookie.length < 1 ? `Bearer ${info.session.access_token}` : info.session.auth_apisid;
|
||||
|
||||
if (info.session.logged_in) {
|
||||
|
||||
headers.Cookie = info.session.cookie;
|
||||
headers.authorization = info.session.cookie.length && info.session.auth_apisid || `Bearer ${info.session.access_token}`;
|
||||
}
|
||||
|
||||
return req_opts;
|
||||
|
||||
return headers
|
||||
},
|
||||
VIDEO_INFO_REQBODY: (id, sts, context) => {
|
||||
return {
|
||||
@@ -98,7 +91,7 @@ module.exports = {
|
||||
},
|
||||
YTMUSIC_VERSION: '1.20211213.00.00',
|
||||
METADATA_KEYS: [
|
||||
'embed', 'view_count', 'average_rating',
|
||||
'embed', 'view_count', 'average_rating', 'allow_ratings',
|
||||
'length_seconds', 'channel_id', 'channel_url',
|
||||
'external_channel_id', 'is_live_content', 'is_family_safe',
|
||||
'is_unlisted', 'is_private', 'has_ypc_metadata',
|
||||
@@ -108,7 +101,7 @@ module.exports = {
|
||||
],
|
||||
BLACKLISTED_KEYS: [
|
||||
'is_owner_viewing', 'is_unplugged_corpus',
|
||||
'is_crawlable', 'allow_ratings', 'author'
|
||||
'is_crawlable', 'author'
|
||||
],
|
||||
ACCOUNT_SETTINGS: {
|
||||
// Notifications
|
||||
|
||||
355
lib/Innertube.js
355
lib/Innertube.js
@@ -34,13 +34,14 @@ class Innertube {
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const response = await Axios.get(Constants.URLS.YT_BASE_URL, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
|
||||
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not retrieve Innertube session: ${response.message}`);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`);
|
||||
if (data.INNERTUBE_CONTEXT) {
|
||||
this.key = data.INNERTUBE_API_KEY;
|
||||
this.key = data.INNERTUBE_API_KEY;
|
||||
this.version = data.INNERTUBE_API_VERSION;
|
||||
this.context = data.INNERTUBE_CONTEXT;
|
||||
|
||||
this.player_url = data.PLAYER_JS_URL;
|
||||
@@ -51,23 +52,38 @@ class Innertube {
|
||||
this.context.client.gl = 'US';
|
||||
|
||||
/**
|
||||
* @event auth - Fired when signing in to an account.
|
||||
* @event update-credentials - Fired when the access token is no longer valid.
|
||||
* @event Innertube#auth - Fired when signing in to an account.
|
||||
* @event Innertube#update-credentials - Fired when the access token is no longer valid.
|
||||
* @type {EventEmitter}
|
||||
*/
|
||||
this.ev = new EventEmitter();
|
||||
|
||||
this.#player = new Player(this);
|
||||
await this.#player.init();
|
||||
|
||||
if (this.logged_in && this.cookie.length > 1) {
|
||||
|
||||
if (this.logged_in && this.cookie.length) {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.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.#initMethods();
|
||||
} else {
|
||||
throw new Error('Could not retrieve Innertube session due to unknown reasons');
|
||||
throw new Error('No InnerTubeContext shell provided in ytconfig.');
|
||||
}
|
||||
} catch (err) {
|
||||
this.#retry_count += 1;
|
||||
@@ -262,8 +278,16 @@ class Innertube {
|
||||
this.access_token = auth_info.access_token;
|
||||
this.refresh_token = auth_info.refresh_token;
|
||||
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 });
|
||||
|
||||
resolve();
|
||||
resolve();
|
||||
} else {
|
||||
oauth.on('auth', (data) => {
|
||||
if (data.status === 'SUCCESS') {
|
||||
@@ -271,6 +295,13 @@ class Innertube {
|
||||
this.access_token = data.credentials.access_token;
|
||||
this.refresh_token = data.credentials.refresh_token;
|
||||
this.logged_in = true;
|
||||
|
||||
delete this.YTRequester.defaults.params.key;
|
||||
delete this.YTMRequester.defaults.params.key;
|
||||
|
||||
this.YTRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false });
|
||||
this.YTMRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true });
|
||||
|
||||
resolve();
|
||||
} else {
|
||||
this.ev.emit('auth', data);
|
||||
@@ -297,7 +328,7 @@ class Innertube {
|
||||
language: menu.sections[1].multiPageMenuSectionRenderer.items[1].compactLinkRenderer.subtitle.simpleText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches on YouTube.
|
||||
*
|
||||
@@ -313,7 +344,7 @@ class Innertube {
|
||||
async search(query, options = { client: 'YOUTUBE', period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
const response = await Actions.search(this, options.client, { query, options });
|
||||
if (!response.success) throw new Error(`Could not search on YouTube: ${response.message}`);
|
||||
|
||||
|
||||
const data = new Parser(this, response.data, {
|
||||
client: options.client,
|
||||
data_type: 'SEARCH',
|
||||
@@ -327,11 +358,12 @@ class Innertube {
|
||||
* Gets search suggestions.
|
||||
*
|
||||
* @param {string} input - The search query.
|
||||
* @param {string} [client='YOUTUBE'] - Client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`.
|
||||
* @param {object} [options] - Search options.
|
||||
* @param {string} [options.client='YOUTUBE'] - Client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`.
|
||||
* @returns {Promise.<[{ text: string; bold_text: string }]>}
|
||||
*/
|
||||
async getSearchSuggestions(input, { client = 'YOUTUBE' }) {
|
||||
if (client == 'YOUTUBE') {
|
||||
async getSearchSuggestions(input, options = { client: 'YOUTUBE' }) {
|
||||
if (options.client == 'YOUTUBE') {
|
||||
const response = await Actions.getYTSearchSuggestions(this, input);
|
||||
if (!response.success) throw new Error('Could not get search suggestions');
|
||||
|
||||
@@ -341,7 +373,7 @@ class Innertube {
|
||||
bold_text: response.data[0].trim()
|
||||
};
|
||||
});
|
||||
} else if (client == 'YTMUSIC') {
|
||||
} else if (options.client == 'YTMUSIC') {
|
||||
const response = await Actions.music(this, 'get_search_suggestions', { input });
|
||||
if (!response.success) throw new Error('Could not get search suggestions');
|
||||
if (!response.data.contents) return [];
|
||||
@@ -373,32 +405,114 @@ class Innertube {
|
||||
async getDetails(video_id) {
|
||||
if (!video_id) throw new Error('You must provide a video id');
|
||||
|
||||
const data = await Actions.getVideoInfo(this, { id: video_id, is_desktop: false });
|
||||
const refined_data = new Parser(this, data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: false }).parse();
|
||||
const data = await Actions.getVideoInfo(this, { id: video_id });
|
||||
const continuation = await Actions.next(this, { video_id });
|
||||
data.continuation = continuation.data;
|
||||
|
||||
const details = new Parser(this, data, { 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.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' });
|
||||
|
||||
if (refined_data.metadata.is_live_content) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id });
|
||||
if (data_continuation.data.contents.twoColumnWatchNextResults.conversationBar) {
|
||||
refined_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, refined_data.metadata.channel_id, video_id);
|
||||
} else {
|
||||
refined_data.getLivechat = () => { };
|
||||
}
|
||||
} else {
|
||||
refined_data.getLivechat = () => { };
|
||||
}
|
||||
|
||||
refined_data.like = () => Actions.engage(this, 'like/like', { video_id });
|
||||
refined_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id });
|
||||
refined_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id });
|
||||
refined_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { channel_id: refined_data.metadata.channel_id });
|
||||
refined_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { channel_id: refined_data.metadata.channel_id });
|
||||
refined_data.comment = (text) => Actions.engage(this, 'comment/create_comment', { video_id, text });
|
||||
refined_data.getComments = () => this.getComments(video_id);
|
||||
refined_data.changeNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: refined_data.metadata.channel_id, pref: type || 'NONE' });
|
||||
|
||||
return refined_data;
|
||||
return details;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets info about a given channel. (WIP)
|
||||
*
|
||||
* @param {string} id - The id of the channel.
|
||||
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
|
||||
*/
|
||||
async getChannel(id) {
|
||||
const response = await Actions.browse(this, 'channel', { browse_id: id });
|
||||
if (!response.success) throw new Error('Could not retrieve channel info.');
|
||||
|
||||
const tabs = response.data.contents.twoColumnBrowseResultsRenderer.tabs;
|
||||
const metadata = response.data.metadata;
|
||||
|
||||
const home_tab = tabs.find((tab) => tab.tabRenderer.title == 'Home');
|
||||
const home_contents = home_tab.tabRenderer.content.sectionListRenderer.contents;
|
||||
const home_shelves = [];
|
||||
|
||||
home_contents.forEach((content) => {
|
||||
if(!content.itemSectionRenderer) return;
|
||||
|
||||
const contents = content.itemSectionRenderer.contents[0];
|
||||
|
||||
const list = contents?.shelfRenderer?.content.horizontalListRenderer;
|
||||
if (!list) return; // Will support only videos & playlists for now
|
||||
|
||||
const shelf = {
|
||||
title: contents.shelfRenderer.title.runs[0].text,
|
||||
content: []
|
||||
};
|
||||
|
||||
shelf.content = list.items.map((item) => {
|
||||
const renderer = item.gridVideoRenderer || item.gridPlaylistRenderer;
|
||||
if (renderer.videoId) {
|
||||
return {
|
||||
id: renderer?.videoId,
|
||||
title: renderer?.title?.simpleText,
|
||||
metadata: {
|
||||
view_count: renderer?.viewCountText?.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: renderer?.shortViewCountText?.simpleText || 'N/A',
|
||||
accessibility_label: renderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A',
|
||||
},
|
||||
thumbnail: renderer?.thumbnail?.thumbnails?.slice(-1)[0] || {},
|
||||
moving_thumbnail: renderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {},
|
||||
published: renderer?.publishedTimeText?.simpleText || 'N/A',
|
||||
badges: renderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: renderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: renderer?.playlistId,
|
||||
title: renderer?.title?.runs?.map((run) => run.text).join(''),
|
||||
metadata: {
|
||||
thumbnail: renderer?.thumbnail?.thumbnails?.slice(-1)[0] || {},
|
||||
video_count: renderer?.videoCountShortText?.simpleText || 'N/A',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
home_shelves.push(shelf);
|
||||
});
|
||||
|
||||
return {
|
||||
title: metadata.channelMetadataRenderer.title,
|
||||
description: metadata.channelMetadataRenderer.description,
|
||||
metadata: {
|
||||
url: metadata.channelMetadataRenderer?.channelUrl,
|
||||
rss_urls: metadata.channelMetadataRenderer?.rssUrl,
|
||||
vanity_channel_url: metadata.channelMetadataRenderer?.vanityChannelUrl,
|
||||
external_id: metadata.channelMetadataRenderer?.externalId,
|
||||
is_family_safe: metadata.channelMetadataRenderer?.isFamilySafe,
|
||||
keywords: metadata.channelMetadataRenderer?.keywords
|
||||
},
|
||||
content: {
|
||||
// Home page of the channel, always available in the first request.
|
||||
home_page: home_shelves,
|
||||
|
||||
// Functions— these will need additional requests and will possibly use the parser.
|
||||
getVideos: () => {},
|
||||
getPlaylists: () => {},
|
||||
getCommunity: () => {},
|
||||
getChannels: () => {},
|
||||
getAbout: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the lyrics for a given song if available.
|
||||
*
|
||||
@@ -406,9 +520,9 @@ class Innertube {
|
||||
* @returns {Promise.<string>} Song lyrics
|
||||
*/
|
||||
async getLyrics(video_id) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id: video_id, ytmusic: true });
|
||||
const continuation = await Actions.next(this, { video_id: video_id, ytmusic: true });
|
||||
|
||||
const lyrics_tab = data_continuation.data.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer
|
||||
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 });
|
||||
@@ -438,52 +552,81 @@ class Innertube {
|
||||
* Gets the comments section of a video.
|
||||
*
|
||||
* @param {string} video_id - The id of the video.
|
||||
* @param {string} [token] - Continuation token (optional).
|
||||
* @param {string} [data] - Video data and continuation token (optional).
|
||||
* @return {Promise.<[{ comments: []; comment_count: string }]>
|
||||
*/
|
||||
async getComments(video_id, token) {
|
||||
async getComments(video_id, data = {}) {
|
||||
let comment_section_token;
|
||||
|
||||
if (!token) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id });
|
||||
const item_section_renderer = data_continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer);
|
||||
|
||||
if (!data.token) {
|
||||
const continuation = await Actions.next(this, { video_id });
|
||||
|
||||
const item_section_renderer = continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer);
|
||||
comment_section_token = item_section_renderer.itemSectionRenderer.contents[0].continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const secondary_info_renderer = continuation.data.contents.twoColumnWatchNextResults
|
||||
.results.results.contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer;
|
||||
|
||||
data.channel_id = secondary_info_renderer.owner.videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId;
|
||||
}
|
||||
|
||||
const response = await Actions.getContinuation(this, { continuation_token: comment_section_token || token });
|
||||
|
||||
const response = await Actions.next(this, { continuation_token: comment_section_token || data.token });
|
||||
if (!response.success) throw new Error('Could not fetch comments section');
|
||||
|
||||
|
||||
const comments_section = { comments: [] };
|
||||
!token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A');
|
||||
|
||||
!data.token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A');
|
||||
|
||||
let continuation_token;
|
||||
!token && (continuation_token = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token) ||
|
||||
(continuation_token = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token);
|
||||
|
||||
comments_section.getContinuation = () => this.getComments(video_id, continuation_token);
|
||||
!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;
|
||||
!token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems) ||
|
||||
!data.token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems) ||
|
||||
(contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
contents.forEach((thread) => {
|
||||
if (!thread.commentThreadRenderer) return;
|
||||
|
||||
contents.forEach((content) => {
|
||||
const thread = content?.commentThreadRenderer?.comment.commentRenderer || content?.commentRenderer;
|
||||
if (!thread) return;
|
||||
|
||||
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.commentThreadRenderer.comment.commentRenderer.contentText.runs.map((t) => t.text).join(' '),
|
||||
text: thread.contentText.runs.map((t) => t.text).join(' '),
|
||||
author: {
|
||||
name: thread.commentThreadRenderer.comment.commentRenderer.authorText.simpleText,
|
||||
thumbnail: thread.commentThreadRenderer.comment.commentRenderer.authorThumbnail.thumbnails,
|
||||
channel_id: thread.commentThreadRenderer.comment.commentRenderer.authorEndpoint.browseEndpoint.browseId
|
||||
name: thread.authorText.simpleText,
|
||||
thumbnail: thread.authorThumbnail.thumbnails,
|
||||
channel_id: thread.authorEndpoint.browseEndpoint.browseId
|
||||
},
|
||||
metadata: {
|
||||
published: thread.commentThreadRenderer.comment.commentRenderer.publishedTimeText.runs[0].text,
|
||||
is_liked: thread.commentThreadRenderer.comment.commentRenderer.isLiked,
|
||||
is_channel_owner: thread.commentThreadRenderer.comment.commentRenderer.authorIsChannelOwner,
|
||||
like_count: thread.commentThreadRenderer.comment.commentRenderer.voteCount && thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText || 'N/A',
|
||||
reply_count: thread.commentThreadRenderer.comment.commentRenderer.replyCount || 0,
|
||||
id: thread.commentThreadRenderer.comment.commentRenderer.commentId,
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -508,15 +651,27 @@ class Innertube {
|
||||
const content = {
|
||||
id: item.videoRenderer.videoId,
|
||||
title: item.videoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
channel: item.videoRenderer.shortBylineText && item.videoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
description: item.videoRenderer.descriptionSnippet && item.videoRenderer.descriptionSnippet.runs[0].text || 'N/A',
|
||||
channel: {
|
||||
name: item.videoRenderer.shortBylineText && item.videoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
url: item.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
|
||||
},
|
||||
metadata: {
|
||||
view_count: item.videoRenderer.viewCountText && item.videoRenderer.viewCountText.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: item.videoRenderer.shortViewCountText && item.videoRenderer.shortViewCountText.simpleText || 'N/A',
|
||||
accessibility_label: item.videoRenderer.shortViewCountText && (item.videoRenderer.shortViewCountText.accessibility && item.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
|
||||
},
|
||||
thumbnail: item.videoRenderer.thumbnail && item.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
|
||||
moving_thumbnail: item.videoRenderer.richThumbnail && item.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || [],
|
||||
published: item.videoRenderer.publishedTimeText && item.videoRenderer.publishedTimeText.simpleText || 'N/A',
|
||||
duration: item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || 'N/A',
|
||||
badges: item.videoRenderer.badges && item.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
|
||||
owner_badges: item.videoRenderer.ownerBadges && item.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
|
||||
duration: {
|
||||
seconds: Utils.timeToSeconds(item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || '0'),
|
||||
simple_text: item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || 'N/A',
|
||||
accessibility_label: item.videoRenderer.lengthText && item.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A'
|
||||
},
|
||||
badges: item.videoRenderer.badges && item.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: item.videoRenderer.ownerBadges && item.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
};
|
||||
history.push(content);
|
||||
@@ -527,7 +682,7 @@ class Innertube {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns YouTube's home feed.
|
||||
* Returns YouTube's home feed (aka recommendations).
|
||||
* @returns {Promise.<[{ id: string; title: string; channel: string; metadata: {} }]>}
|
||||
*/
|
||||
async getHomeFeed() {
|
||||
@@ -535,7 +690,7 @@ class Innertube {
|
||||
if (!response.success) throw new Error('Could not get home feed');
|
||||
|
||||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.richGridRenderer.contents;
|
||||
|
||||
|
||||
return contents.map((item) => {
|
||||
const content = item.richItemRenderer && item.richItemRenderer.content.videoRenderer &&
|
||||
item.richItemRenderer.content || undefined;
|
||||
@@ -543,12 +698,25 @@ class Innertube {
|
||||
if (content) return {
|
||||
id: content.videoRenderer.videoId,
|
||||
title: content.videoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
channel: content.videoRenderer.shortBylineText && content.videoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
description: content.videoRenderer.descriptionSnippet && content.videoRenderer.descriptionSnippet.runs[0].text || 'N/A',
|
||||
channel: {
|
||||
name: content.videoRenderer.shortBylineText && content.videoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
url: content.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${content.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
|
||||
},
|
||||
metadata: {
|
||||
view_count: content.videoRenderer.viewCountText && content.videoRenderer.viewCountText.simpleText || 'N/A',
|
||||
thumbnail: content.videoRenderer.thumbnail && content.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
|
||||
moving_thumbnail: content.videoRenderer.richThumbnail && content.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || [],
|
||||
short_view_count_text: {
|
||||
simple_text: content.videoRenderer.shortViewCountText && content.videoRenderer.shortViewCountText.simpleText || 'N/A',
|
||||
accessibility_label: content.videoRenderer.shortViewCountText && (content.videoRenderer.shortViewCountText.accessibility && content.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
|
||||
},
|
||||
thumbnail: content.videoRenderer.thumbnail && content.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || {},
|
||||
moving_thumbnail: content.videoRenderer.richThumbnail && content.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {},
|
||||
published: content.videoRenderer.publishedTimeText && content.videoRenderer.publishedTimeText.simpleText || 'N/A',
|
||||
duration: {
|
||||
seconds: Utils.timeToSeconds(content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || '0'),
|
||||
simple_text: content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || 'N/A',
|
||||
accessibility_label: content.videoRenderer.lengthText && content.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A'
|
||||
},
|
||||
badges: content.videoRenderer.badges && content.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: content.videoRenderer.ownerBadges && content.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
@@ -580,13 +748,21 @@ class Innertube {
|
||||
const content = {
|
||||
id: item.gridVideoRenderer.videoId,
|
||||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
channel: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
channel: {
|
||||
name: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
url: item.gridVideoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.gridVideoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
|
||||
},
|
||||
metadata: {
|
||||
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: item.gridVideoRenderer.shortViewCountText && item.gridVideoRenderer.shortViewCountText.simpleText || 'N/A',
|
||||
accessibility_label: item.gridVideoRenderer.shortViewCountText && (item.gridVideoRenderer.shortViewCountText.accessibility && item.gridVideoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
|
||||
},
|
||||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
|
||||
moving_thumbnail: item.gridVideoRenderer.richThumbnail && item.gridVideoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {},
|
||||
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A',
|
||||
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
|
||||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
|
||||
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -620,7 +796,7 @@ class Innertube {
|
||||
read: notification.read,
|
||||
notification_id: notification.notificationId,
|
||||
};
|
||||
}).filter((notification_block) => notification_block);
|
||||
}).filter((notification) => notification);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,7 +833,10 @@ class Innertube {
|
||||
const url_components = new URL(format.url);
|
||||
url_components.searchParams.set('cver', this.context.client.clientVersion);
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc).transform(url_components.searchParams.get('n')));
|
||||
|
||||
if (url_components.searchParams.get('n')) {
|
||||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc).transform(url_components.searchParams.get('n')));
|
||||
}
|
||||
|
||||
format.url = url_components.toString();
|
||||
format.has_audio = !!format.audioBitrate || !!format.audioQuality;
|
||||
@@ -715,7 +894,7 @@ class Innertube {
|
||||
options.type = options.type || 'videoandaudio';
|
||||
options.format = options.format || 'mp4';
|
||||
|
||||
const data = await Actions.getVideoInfo(this, { id, desktop: true });
|
||||
const data = await Actions.getVideoInfo(this, { id });
|
||||
const streaming_data = this.#chooseFormat(options, data);
|
||||
if (!streaming_data.selected_format) throw new Error('Could not find any suitable format.');
|
||||
|
||||
@@ -743,7 +922,7 @@ class Innertube {
|
||||
let cancelled = false;
|
||||
|
||||
const stream = new Stream.PassThrough();
|
||||
Actions.getVideoInfo(this, { id, desktop: true }).then(async (video_data) => {
|
||||
Actions.getVideoInfo(this, { id }).then(async (video_data) => {
|
||||
if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED')
|
||||
return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status });
|
||||
if (!video_data.streamingData)
|
||||
@@ -872,4 +1051,4 @@ class Innertube {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Innertube;
|
||||
module.exports = Innertube;
|
||||
|
||||
@@ -6,6 +6,10 @@ const EventEmitter = require('events');
|
||||
class Livechat extends EventEmitter {
|
||||
constructor(session, token, channel_id, video_id) {
|
||||
super(session);
|
||||
|
||||
if (!token)
|
||||
throw new Error('Could not retrieve livechat data');
|
||||
|
||||
this.ctoken = token;
|
||||
this.session = session;
|
||||
this.video_id = video_id;
|
||||
@@ -16,7 +20,7 @@ class Livechat extends EventEmitter {
|
||||
|
||||
this.poll_intervals_ms = 1000;
|
||||
this.running = true;
|
||||
|
||||
|
||||
this.#poll();
|
||||
}
|
||||
|
||||
|
||||
17
lib/OAuth.js
17
lib/OAuth.js
@@ -11,10 +11,9 @@ class OAuth extends EventEmitter {
|
||||
this.auth_info = auth_info;
|
||||
this.refresh_interval = 5;
|
||||
|
||||
this.oauth_code_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/device/code`;
|
||||
this.oauth_token_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/token`;
|
||||
this.guide_url = `${Constants.URLS.YT_BASE_URL}/youtubei/v1/guide`;
|
||||
|
||||
this.oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`;
|
||||
this.oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`;
|
||||
|
||||
this.model_name = Constants.OAUTH.MODEL_NAME;
|
||||
this.grant_type = Constants.OAUTH.GRANT_TYPE;
|
||||
this.scope = Constants.OAUTH.SCOPE;
|
||||
@@ -124,7 +123,7 @@ class OAuth extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Gets a new access token using a refresh token.
|
||||
* @returns {object.<{ credentials: { access_token: string; refresh_token: string; expires: Date }; status: string }>}
|
||||
* @returns {Promise.<{ credentials: { access_token: string; refresh_token: string; expires: Date }; status: string }>}
|
||||
*/
|
||||
async refreshAccessToken() {
|
||||
const identity = await this.#getClientIdentity();
|
||||
@@ -172,13 +171,13 @@ class OAuth extends EventEmitter {
|
||||
*/
|
||||
async #getClientIdentity() {
|
||||
// This request is made to get the auth script url, hard-coding it isn't viable as it changes overtime.
|
||||
const yttv_response = await Axios.get(`${Constants.URLS.YT_BASE_URL}/tv`, Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
const yttv_response = await Axios.get(`${Constants.URLS.YT_BASE}/tv`, Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (yttv_response instanceof Error) throw new Error(`Could not extract client identity: ${yttv_response.message}`);
|
||||
|
||||
// Here we download the script and extract the necessary data to proceed with the auth flow.
|
||||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1];
|
||||
const script_url = `${Constants.URLS.YT_BASE_URL}/${url_body}`;
|
||||
|
||||
const script_url = `${Constants.URLS.YT_BASE}/${url_body}`;
|
||||
|
||||
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}`);
|
||||
|
||||
@@ -197,4 +196,4 @@ class OAuth extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuth;
|
||||
module.exports = OAuth;
|
||||
|
||||
104
lib/Parser.js
104
lib/Parser.js
@@ -40,17 +40,18 @@ class Parser {
|
||||
response.query = contents[0].showingResultsForRenderer && contents[0].showingResultsForRenderer.originalQuery.simpleText || this.args.query;
|
||||
response.corrected_query = contents[0].showingResultsForRenderer && contents[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query || this.args.query;
|
||||
response.estimated_results = parseInt(this.data.estimatedResults);
|
||||
|
||||
response.getContinuation = () => {};
|
||||
|
||||
response.videos = contents.map((data) => {
|
||||
if (!data.videoRenderer) return;
|
||||
const video = data.videoRenderer;
|
||||
return {
|
||||
id: video.videoId,
|
||||
title: video.title.runs[0].text,
|
||||
description: video.detailedMetadataSnippets && video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') || 'N/A',
|
||||
channel_url: `${Constants.URLS.YT_BASE}${video.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,
|
||||
author: video.ownerText.runs[0].text,
|
||||
id: video.videoId,
|
||||
url: `https://youtu.be/${video.videoId}`,
|
||||
channel_url: `${Constants.URLS.YT_BASE_URL}${video.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,
|
||||
metadata: {
|
||||
view_count: video.viewCountText && video.viewCountText.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
@@ -124,7 +125,7 @@ class Parser {
|
||||
title: list_item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
|
||||
artist: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text,
|
||||
album: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.find((run) => /^\d+$/.test(run.text.replace(/:/g, ''))).text,
|
||||
thumbnail: list_item.thumbnail.musicThumbnailRenderer.thumbnail,
|
||||
getLyrics: () => this.session.getLyrics(list_item.playlistItemData.videoId)
|
||||
};
|
||||
@@ -139,7 +140,7 @@ class Parser {
|
||||
title: list_item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
|
||||
author: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text,
|
||||
views: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.find((run) => /^\d+$/.test(run.text.replace(/:/g, ''))).text,
|
||||
thumbnail: list_item.thumbnail.musicThumbnailRenderer.thumbnail,
|
||||
getLyrics: () => this.session.getLyrics(list_item.playlistItemData.videoId)
|
||||
};
|
||||
@@ -207,25 +208,20 @@ class Parser {
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Video data is parsed dynamically, so if youtube decides to add something new we won't have to change anything here.
|
||||
*/
|
||||
#parseVideoInfo() {
|
||||
const desktop_v = this.args.desktop_v;
|
||||
|
||||
const playability_status = desktop_v && this.data.playabilityStatus ||
|
||||
this.data[2].playerResponse.playabilityStatus;
|
||||
const playability_status = this.data.playabilityStatus;
|
||||
|
||||
if (playability_status.status == 'ERROR')
|
||||
throw new Error(`Could not retrieve details for this video: ${playability_status.status} - ${playability_status.reason}`);
|
||||
|
||||
const details = desktop_v && this.data.videoDetails ||
|
||||
this.data[2].playerResponse.videoDetails;
|
||||
|
||||
const microformat = desktop_v && this.data.microformat.playerMicroformatRenderer ||
|
||||
this.data[2].playerResponse.microformat.playerMicroformatRenderer;
|
||||
|
||||
const streaming_data = desktop_v && this.data.streamingData ||
|
||||
this.data[2].playerResponse.streamingData;
|
||||
throw new Error(`Could not retrieve video details: ${playability_status.status} - ${playability_status.reason}`);
|
||||
|
||||
const details = this.data.videoDetails;
|
||||
const microformat = this.data.microformat.playerMicroformatRenderer;
|
||||
const streaming_data = this.data.streamingData;
|
||||
|
||||
const response = {
|
||||
id: '',
|
||||
title: '',
|
||||
@@ -236,7 +232,8 @@ class Parser {
|
||||
|
||||
const mf_raw_data = Object.entries(microformat);
|
||||
const dt_raw_data = Object.entries(details);
|
||||
|
||||
|
||||
// Extracts most of the metadata
|
||||
mf_raw_data.forEach((entry) => {
|
||||
const key = Utils.camelToSnake(entry[0]);
|
||||
if (Constants.METADATA_KEYS.includes(key)) {
|
||||
@@ -248,7 +245,8 @@ class Parser {
|
||||
response[key] = entry[1];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Extracts extra details
|
||||
dt_raw_data.forEach((entry) => {
|
||||
const key = Utils.camelToSnake(entry[0]);
|
||||
if (Constants.BLACKLISTED_KEYS.includes(key)) return;
|
||||
@@ -262,26 +260,54 @@ class Parser {
|
||||
(response[key] = entry[1]);
|
||||
}
|
||||
});
|
||||
|
||||
if (!desktop_v) {
|
||||
const dislike_available = this.data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1]
|
||||
.slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer
|
||||
.button.toggleButtonRenderer.defaultText.accessibility && true || false;
|
||||
|
||||
response.metadata.likes = parseInt(this.data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1]
|
||||
.slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[0].slimMetadataToggleButtonRenderer
|
||||
.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
|
||||
response.metadata.dislikes = dislike_available && parseInt(this.data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1]
|
||||
.slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer
|
||||
.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, '')) || 0;
|
||||
|
||||
// Data continuation is only required in getDetails()
|
||||
if (this.data.continuation) {
|
||||
const primary_info_renderer = this.data.continuation.contents.twoColumnWatchNextResults
|
||||
.results.results.contents.find((item) => item.videoPrimaryInfoRenderer).videoPrimaryInfoRenderer;
|
||||
|
||||
const secondary_info_renderer = this.data.continuation.contents.twoColumnWatchNextResults
|
||||
.results.results.contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer;
|
||||
|
||||
const like_btn = primary_info_renderer.videoActions.menuRenderer
|
||||
.topLevelButtons.find((item) => item.toggleButtonRenderer.defaultIcon.iconType == 'LIKE');
|
||||
|
||||
const dislike_btn = primary_info_renderer.videoActions.menuRenderer
|
||||
.topLevelButtons.find((item) => item.toggleButtonRenderer.defaultIcon.iconType == 'DISLIKE');
|
||||
|
||||
const notification_toggle_btn = secondary_info_renderer.subscribeButton.subscribeButtonRenderer
|
||||
?.notificationPreferenceButton?.subscriptionNotificationToggleButtonRenderer;
|
||||
|
||||
// These will always be false if logged out.
|
||||
response.metadata.is_liked = like_btn.toggleButtonRenderer.isToggled;
|
||||
response.metadata.is_disliked = dislike_btn.toggleButtonRenderer.isToggled;
|
||||
response.metadata.is_subscribed = secondary_info_renderer.subscribeButton.subscribeButtonRenderer?.subscribed || false;
|
||||
|
||||
response.metadata.subscriber_count = secondary_info_renderer.owner.videoOwnerRenderer?.subscriberCountText?.simpleText || 'N/A';
|
||||
response.metadata.current_notification_preference = notification_toggle_btn?.states.find((state) => state.stateId == notification_toggle_btn.currentStateId)
|
||||
.state.buttonRenderer.icon.iconType || 'N/A';
|
||||
|
||||
// Simpler version of publish_date
|
||||
response.metadata.publish_date_text = primary_info_renderer.dateText.simpleText;
|
||||
|
||||
// Only parse like count if it's enabled
|
||||
if (response.metadata.allow_ratings) {
|
||||
response.metadata.likes = {
|
||||
count: parseInt(like_btn.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, '')),
|
||||
short_count_text: like_btn.toggleButtonRenderer.defaultText.simpleText
|
||||
};
|
||||
}
|
||||
|
||||
response.metadata.owner_badges = secondary_info_renderer.owner.videoOwnerRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [];
|
||||
}
|
||||
|
||||
response.metadata.available_qualities = [...new Set(streaming_data.adaptiveFormats.filter(v => v.qualityLabel)
|
||||
.map(v => v.qualityLabel).sort((a, b) => + a.replace(/\D/gi, '') - + b.replace(/\D/gi, '')))];
|
||||
|
||||
streaming_data && streaming_data.adaptiveFormats &&
|
||||
(response.metadata.available_qualities = [...new Set(streaming_data.adaptiveFormats.filter(v => v.qualityLabel)
|
||||
.map(v => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, '')))]) ||
|
||||
(response.metadata.available_qualities = []);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Parser;
|
||||
module.exports = Parser;
|
||||
|
||||
@@ -18,7 +18,7 @@ class Player {
|
||||
this.sig_decipher_sc = this.#getSigDecipherCode(player_data);
|
||||
this.ntoken_sc = this.#getNEncoder(player_data);
|
||||
} else {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE_URL}${this.session.player_url}`, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE}${this.session.player_url}`, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error('Could not download player script: ' + response.message);
|
||||
|
||||
try {
|
||||
|
||||
96
lib/Utils.js
96
lib/Utils.js
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Proto = require('protons');
|
||||
const Crypto = require('crypto');
|
||||
const UserAgent = require('user-agents');
|
||||
|
||||
@@ -81,99 +80,6 @@ function camelToSnake(string) {
|
||||
return string[0].toLowerCase() + string.slice(1, string.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes notification preferences protobuf.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {string} index
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeNotificationPref(channel_id, index) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.NotificationPreferences.encode({
|
||||
channel_id,
|
||||
pref_id: {
|
||||
index
|
||||
},
|
||||
number_0: 0,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes livestream message protobuf.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {string} video_id
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeMessageParams(channel_id, video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.LiveMessageParams.encode({
|
||||
params: {
|
||||
ids: {
|
||||
channel_id,
|
||||
video_id
|
||||
}
|
||||
},
|
||||
number_0: 1,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment params protobuf.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeCommentParams(video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.CreateCommentParams.encode({
|
||||
video_id,
|
||||
params: {
|
||||
index: 0
|
||||
},
|
||||
number: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes search filter protobuf
|
||||
*
|
||||
* @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 encodeFilter(period, duration, order) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
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 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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the ntoken transform data into a valid json array
|
||||
*
|
||||
@@ -190,4 +96,4 @@ function refineNTokenData(data) {
|
||||
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
|
||||
}
|
||||
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, encodeMessageParams, encodeCommentParams, encodeNotificationPref, encodeFilter, refineNTokenData };
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
|
||||
133
lib/proto/index.js
Normal file
133
lib/proto/index.js
Normal file
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Proto = require('protons');
|
||||
|
||||
/**
|
||||
* 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`));
|
||||
|
||||
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 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`));
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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 };
|
||||
@@ -1,44 +1,75 @@
|
||||
syntax = "proto2";
|
||||
package proto;
|
||||
|
||||
message NotificationPreferences {
|
||||
string channel_id = 1;
|
||||
message Preference {
|
||||
int32 index = 1;
|
||||
}
|
||||
Preference pref_id = 2;
|
||||
int32 number_0 = 3;
|
||||
int32 number_1 = 4;
|
||||
}
|
||||
|
||||
message LiveMessageParams {
|
||||
message Params {
|
||||
message Ids {
|
||||
string channel_id = 1;
|
||||
string video_id = 2;
|
||||
}
|
||||
Ids ids = 5;
|
||||
}
|
||||
Params params = 1;
|
||||
int32 number_0 = 2;
|
||||
int32 number_1 = 3;
|
||||
}
|
||||
|
||||
message CreateCommentParams {
|
||||
string video_id = 2;
|
||||
message Params {
|
||||
int32 index = 1;
|
||||
}
|
||||
Params params = 5;
|
||||
int32 number = 10;
|
||||
}
|
||||
|
||||
message SearchFilter {
|
||||
int32 number = 1;
|
||||
message Filter {
|
||||
int32 param_0 = 1;
|
||||
int32 param_1 = 2;
|
||||
int32 param_2 = 3;
|
||||
}
|
||||
Filter filter = 2;
|
||||
syntax = "proto2";
|
||||
package proto;
|
||||
|
||||
message NotificationPreferences {
|
||||
string channel_id = 1;
|
||||
message Preference {
|
||||
int32 index = 1;
|
||||
}
|
||||
Preference pref_id = 2;
|
||||
int32 number_0 = 3;
|
||||
int32 number_1 = 4;
|
||||
}
|
||||
|
||||
message LiveMessageParams {
|
||||
message Params {
|
||||
message Ids {
|
||||
string channel_id = 1;
|
||||
string video_id = 2;
|
||||
}
|
||||
Ids ids = 5;
|
||||
}
|
||||
Params params = 1;
|
||||
int32 number_0 = 2;
|
||||
int32 number_1 = 3;
|
||||
}
|
||||
|
||||
message CreateCommentParams {
|
||||
string video_id = 2;
|
||||
message Params {
|
||||
int32 index = 1;
|
||||
}
|
||||
Params params = 5;
|
||||
int32 number = 10;
|
||||
}
|
||||
|
||||
message CreateCommentReplyParams {
|
||||
string video_id = 2;
|
||||
string comment_id = 4;
|
||||
|
||||
message UnknownParams {
|
||||
int32 unk_num = 1;
|
||||
}
|
||||
UnknownParams params = 5;
|
||||
|
||||
int32 unk_num = 10;
|
||||
}
|
||||
|
||||
message PeformCommentActionParams {
|
||||
int32 type = 1;
|
||||
int32 unk_num = 2;
|
||||
|
||||
string comment_id = 3;
|
||||
string video_id = 5;
|
||||
|
||||
int32 unk_num_1 = 6;
|
||||
int32 unk_num_2 = 7;
|
||||
|
||||
string unk_num_3 = 9;
|
||||
|
||||
int32 unk_num_4 = 10;
|
||||
int32 unk_num_5 = 21;
|
||||
|
||||
string channel_id = 23;
|
||||
int32 unk_num_6 = 30;
|
||||
}
|
||||
|
||||
message SearchFilter {
|
||||
int32 number = 1;
|
||||
message Filter {
|
||||
int32 param_0 = 1;
|
||||
int32 param_1 = 2;
|
||||
int32 param_2 = 3;
|
||||
}
|
||||
Filter filter = 2;
|
||||
}
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -1,18 +1,21 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"protons": "^2.0.3",
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/luanrt"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
@@ -53,9 +56,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -127,9 +130,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.0.943",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.943.tgz",
|
||||
"integrity": "sha512-0qfnGXlAO9jbfiFAnhKG3CIWxyfr7s5WgwVlV7ns2jnl8BwQI0rTJ4bdx0HR7FasInT9TCvsulmZgLgBQHbkZA==",
|
||||
"version": "1.0.971",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.971.tgz",
|
||||
"integrity": "sha512-5gwiuDE6Rmi4YRf1wDedduBwkv1RwdveW295+qMbdnWhc6CFSeVceQ2rpYxP/m022E0f45Z7ednPjerh9SMuXw==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -179,9 +182,9 @@
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
@@ -239,9 +242,9 @@
|
||||
}
|
||||
},
|
||||
"user-agents": {
|
||||
"version": "1.0.943",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.943.tgz",
|
||||
"integrity": "sha512-0qfnGXlAO9jbfiFAnhKG3CIWxyfr7s5WgwVlV7ns2jnl8BwQI0rTJ4bdx0HR7FasInT9TCvsulmZgLgBQHbkZA==",
|
||||
"version": "1.0.971",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.971.tgz",
|
||||
"integrity": "sha512-5gwiuDE6Rmi4YRf1wDedduBwkv1RwdveW295+qMbdnWhc6CFSeVceQ2rpYxP/m022E0f45Z7ednPjerh9SMuXw==",
|
||||
"requires": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
|
||||
12
package.json
12
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.8",
|
||||
"description": "An object-oriented library that allows you to search, get detailed info about videos, subscribe, unsubscribe, like, dislike, comment, download videos and much more!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node test"
|
||||
},
|
||||
"author": "LuanRT",
|
||||
"funding": "https://ko-fi.com/luanrt",
|
||||
"license": "MIT",
|
||||
"directories": {
|
||||
"example": "examples",
|
||||
@@ -24,6 +25,7 @@
|
||||
},
|
||||
"keywords": [
|
||||
"yt",
|
||||
"ytdl",
|
||||
"youtube",
|
||||
"youtube-dl",
|
||||
"youtubedl",
|
||||
@@ -31,13 +33,13 @@
|
||||
"innertube",
|
||||
"innertubeapi",
|
||||
"livechat",
|
||||
"dl",
|
||||
"api",
|
||||
"like",
|
||||
"downloader",
|
||||
"dislike",
|
||||
"search",
|
||||
"comment",
|
||||
"downloader"
|
||||
"like",
|
||||
"api",
|
||||
"dl"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/LuanRT/YouTube.js/issues"
|
||||
|
||||
Reference in New Issue
Block a user