mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
389b0f362f | ||
|
|
6ce4a89766 | ||
|
|
4d7573c46f | ||
|
|
445de3546d | ||
|
|
3b265119d6 | ||
|
|
933ec50a1c | ||
|
|
922ab51a8d | ||
|
|
330943cbc0 | ||
|
|
368dfc4ea3 | ||
|
|
3a9d8a411e | ||
|
|
47ea630329 | ||
|
|
c8e8f34a83 | ||
|
|
14a5b0f0e8 | ||
|
|
e817eb46d4 | ||
|
|
6ca6e22fea | ||
|
|
d11d03bd92 | ||
|
|
27962242b7 | ||
|
|
f54993e3b7 | ||
|
|
d2ec5ebe9c | ||
|
|
10abf386b4 | ||
|
|
f0360cac69 | ||
|
|
24c11f2c06 | ||
|
|
ff2ca5ad3b | ||
|
|
391a4300c1 | ||
|
|
530e310da6 | ||
|
|
73bac32886 |
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
node-version: [ 12.x, 14.x, 15.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
||||
302
README.md
302
README.md
@@ -8,18 +8,19 @@ An object-oriented wrapper around the Innertube API, which is what YouTube itsel
|
||||
|
||||
#### What can it do?
|
||||
|
||||
As of now, this is one of the most advanced & stable YouTube libraries out there, and it can:
|
||||
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of what it can do:
|
||||
|
||||
- Search
|
||||
- Get detailed info about videos
|
||||
- Search videos
|
||||
- Get detailed info about any video
|
||||
- Fetch live chat & live stats in real time
|
||||
- Fetch notifications
|
||||
- Fetch subscriptions feed
|
||||
- Change notifications preferences for a channel
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment
|
||||
- Easily sign into your account without having to use cookies!
|
||||
- Easily sign into your account in an easy & reliable way.
|
||||
- Last but not least, you can also download videos!
|
||||
|
||||
Do note that you must be signed-in to perform actions that involve an account, like commenting, subscribing, sending messages to a live chat, etc.
|
||||
Do note that you must be signed-in to perform actions that involve an account, such as commenting, subscribing, sending messages to a live chat, etc.
|
||||
|
||||
#### Do I need an API key to use this?
|
||||
|
||||
@@ -127,7 +128,7 @@ const search = await youtube.search('Looking for life on Mars - Documentary');
|
||||
|
||||
|
||||
|
||||
Getting details about a specific video:
|
||||
Get details about a specific video:
|
||||
|
||||
```js
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
@@ -187,8 +188,271 @@ const video = await youtube.getDetails(search.videos[0].id);
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get comments:
|
||||
|
||||
Fetching notifications:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
const comments = await video.getComments();
|
||||
|
||||
// If you want to load more comments simply call:
|
||||
const comments_continuation = await comments.getContinuation();
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
<p>
|
||||
|
||||
```js
|
||||
{
|
||||
"comments":[
|
||||
{
|
||||
"text":"The amazing thing to me is the engineering. It's truly remarkable that we can build machines like these.",
|
||||
"author":{
|
||||
"name":"Mark B",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s48-c-k-c0x00ffffff-no-rj",
|
||||
"width":48,
|
||||
"height":48
|
||||
},
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s88-c-k-c0x00ffffff-no-rj",
|
||||
"width":88,
|
||||
"height":88
|
||||
},
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLTKxmup9YqNEMvf-nSdOe7F6CwWhUtu4mpUsg=s176-c-k-c0x00ffffff-no-rj",
|
||||
"width":176,
|
||||
"height":176
|
||||
}
|
||||
],
|
||||
"channel_id":"UClnPXUOtCLnKsbS2reuN7wg"
|
||||
},
|
||||
"metadata":{
|
||||
"published":"2 months ago",
|
||||
"is_liked":false,
|
||||
"is_channel_owner":false,
|
||||
"like_count":"54",
|
||||
"reply_count":3,
|
||||
"id":"Ugy-bGGepYil_2dAQUp4AaABAg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text":"May 25th, 2021 and everything has gone perfectly! Ingenuity, moxy and perseverance all working to plan. Unbelievable accomplishments!!!",
|
||||
"author":{
|
||||
"name":"cliff luebke",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s48-c-k-c0x00ffffff-no-rj",
|
||||
"width":48,
|
||||
"height":48
|
||||
},
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s88-c-k-c0x00ffffff-no-rj",
|
||||
"width":88,
|
||||
"height":88
|
||||
},
|
||||
{
|
||||
"url":"https://yt3.ggpht.com/ytc/AKedOLR1_6jvPZa_ycrkUEVxVxo0Alo25e7O8fOcm5v9ww=s176-c-k-c0x00ffffff-no-rj",
|
||||
"width":176,
|
||||
"height":176
|
||||
}
|
||||
],
|
||||
"channel_id":"UCeVFeX4jCgaJpvNJ4f_I4RA"
|
||||
},
|
||||
"metadata":{
|
||||
"published":"4 months ago",
|
||||
"is_liked":false,
|
||||
"is_channel_owner":false,
|
||||
"like_count":"54",
|
||||
"reply_count":0,
|
||||
"id":"UgylkZHOe7v78hxHPpl4AaABAg"
|
||||
}
|
||||
},
|
||||
//...
|
||||
],
|
||||
"comment_count":"3,231" // not available in continuations
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get subscriptions feed:
|
||||
```js
|
||||
const mysubfeed = await youtube.getSubscriptionsFeed();
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
<p>
|
||||
|
||||
```js
|
||||
{
|
||||
"today":[
|
||||
{
|
||||
"title":"Life As My P*nis",
|
||||
"id":"udDINILQH10",
|
||||
"channel":"penguinz0",
|
||||
"metadata":{
|
||||
"view_count":"220,432 views",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAQ7bbeUhCxRSg-g-CPek-soixUMQ",
|
||||
"width":210,
|
||||
"height":118
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB6fuvBJMeLtkM0TLkZwharsyojjA",
|
||||
"width":246,
|
||||
"height":138
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/udDINILQH10/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDd7BncH1QuZD-Hada_n6dAVRTnmg",
|
||||
"width":336,
|
||||
"height":188
|
||||
}
|
||||
],
|
||||
"published":"2 hours ago",
|
||||
"badges":"N/A",
|
||||
"owner_badges":[
|
||||
"Verified"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title":"Perseverance and Ingenuity went two weeks without contacting Earth",
|
||||
"id":"VsmYZMVCHuc",
|
||||
"channel":"Mars Guy",
|
||||
"metadata":{
|
||||
"view_count":"2,633 views",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA0-vwAgpFbMO1zG4HTzdHZey1kZQ",
|
||||
"width":210,
|
||||
"height":118
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBtM3W-RXsCfPnxgnrktaBkiL9zzg",
|
||||
"width":246,
|
||||
"height":138
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/VsmYZMVCHuc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDTil7At4FUVYeSNySOoFoKlPXWSA",
|
||||
"width":336,
|
||||
"height":188
|
||||
}
|
||||
],
|
||||
"published":"15 hours ago",
|
||||
"badges":"N/A",
|
||||
"owner_badges":"N/A"
|
||||
}
|
||||
}
|
||||
//...
|
||||
],
|
||||
"yesterday":[
|
||||
{
|
||||
"title":"Fortnite - S.T.A.R.S (Resident Evil) | PS5, PS4",
|
||||
"id":"-ZLEQOVbWD4",
|
||||
"channel":"PlayStation",
|
||||
"metadata":{
|
||||
"view_count":"157,197 views",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAeA1fLzsEA0ZIouNJuMDJOqOc9Ng",
|
||||
"width":210,
|
||||
"height":118
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDzvhxJ6m2ztykk2ezNH2Din33hEw",
|
||||
"width":246,
|
||||
"height":138
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/-ZLEQOVbWD4/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDo67DvnaiKzOVfNm9lJg_Edd1UDQ",
|
||||
"width":336,
|
||||
"height":188
|
||||
}
|
||||
],
|
||||
"published":"1 day ago",
|
||||
"badges":"N/A",
|
||||
"owner_badges":[
|
||||
"Verified"
|
||||
]
|
||||
}
|
||||
},
|
||||
//...
|
||||
],
|
||||
"this_week":[
|
||||
{
|
||||
"title":"Horrible $100 Million Gold Mansion",
|
||||
"id":"F-d3CEYJyrg",
|
||||
"channel":"penguinz0",
|
||||
"metadata":{
|
||||
"view_count":"693,041 views",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDdMMGG-50O4U5uIcoZ2FoiO6Mopg",
|
||||
"width":210,
|
||||
"height":118
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAwqMy1ekLaxWGnjWwCJl7z7Nw2aQ",
|
||||
"width":246,
|
||||
"height":138
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/F-d3CEYJyrg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLADaPBg0vh52e6clHvUf5otBZO9HA",
|
||||
"width":336,
|
||||
"height":188
|
||||
}
|
||||
],
|
||||
"published":"2 days ago",
|
||||
"badges":"N/A",
|
||||
"owner_badges":[
|
||||
"Verified"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title":"OOopsieeee",
|
||||
"id":"mJ2WOIhEPm8",
|
||||
"channel":"PewDiePie",
|
||||
"metadata":{
|
||||
"view_count":"1,953,970 views",
|
||||
"thumbnail":[
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEbCNIBEHZIVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCrJe2a6rasJICj_jchMquZ2YGVrQ",
|
||||
"width":210,
|
||||
"height":118
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCpxERsiLJayuKegeb5mHw3Ok6wGA",
|
||||
"width":246,
|
||||
"height":138
|
||||
},
|
||||
{
|
||||
"url":"https://i.ytimg.com/vi/mJ2WOIhEPm8/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAlRLjypimzrE3GD_iGYDGwTlCGvA",
|
||||
"width":336,
|
||||
"height":188
|
||||
}
|
||||
],
|
||||
"published":"2 days ago",
|
||||
"badges":"N/A",
|
||||
"owner_badges":[
|
||||
"Verified"
|
||||
]
|
||||
}
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get notifications:
|
||||
|
||||
```js
|
||||
const notifications = await youtube.getNotifications();
|
||||
@@ -229,7 +493,7 @@ const notifications = await youtube.getNotifications();
|
||||
|
||||
---
|
||||
|
||||
* Subscribing/Unsubscribing to channels:
|
||||
* Subscribe/Unsubscribe:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube
|
||||
|
||||
@@ -237,7 +501,7 @@ await video.subscribe();
|
||||
await video.unsubscribe();
|
||||
```
|
||||
|
||||
* Liking/Disliking:
|
||||
* Like/Dislike:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube
|
||||
|
||||
@@ -246,24 +510,23 @@ await video.dislike();
|
||||
await video.removeLike(); // removes either a like or dislike
|
||||
```
|
||||
|
||||
* Commenting:
|
||||
* Comment:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.comment('Haha, nice!');
|
||||
```
|
||||
|
||||
* Changing notification preferences:
|
||||
* Change notification preferences:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.setNotificationPref('ALL'); // ALL | NONE | PERSONALIZED
|
||||
```
|
||||
|
||||
All of the interactions above will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
|
||||
All of the above interactions will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
|
||||
### Fetching live chats:
|
||||
---
|
||||
YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats in an easy way plus you can also send messages!
|
||||
YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats plus you can also send messages!
|
||||
```js
|
||||
const Innertube = require('youtubei.js');
|
||||
|
||||
@@ -285,7 +548,7 @@ async function start() {
|
||||
livechat.on('chat-update', (message) => {
|
||||
console.info(`- ${message.author.name}\n${message.text}\n\n`);
|
||||
|
||||
If(message.text == '!info') {
|
||||
if(message.text == '!info') {
|
||||
livechat.sendMessage('Hello! This message was sent from YouTube.js');
|
||||
}
|
||||
});
|
||||
@@ -293,6 +556,11 @@ async function start() {
|
||||
|
||||
start();
|
||||
```
|
||||
Stop fetching the live chat:
|
||||
```js
|
||||
livechat.stop();
|
||||
```
|
||||
|
||||
Deleting a message:
|
||||
```js
|
||||
const msg = await livechat.sendMessage('Nice live!');
|
||||
@@ -302,6 +570,8 @@ await msg.deleteMessage();
|
||||
### Downloading videos:
|
||||
---
|
||||
|
||||
YouTube.js provides an easy-to-use and simple downloader:
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
const Innertube = require('youtubei.js');
|
||||
@@ -356,7 +626,7 @@ This library allows you to sign-in in two different ways:
|
||||
- Using OAuth 2.0, easy, simple & reliable.
|
||||
- Cookies, usually more complicated to get and unreliable.
|
||||
|
||||
OAuth 2.0:
|
||||
OAuth:
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -9,10 +9,15 @@ async function start() {
|
||||
// Searching, getting details about videos & making interactions:
|
||||
const search = await youtube.search('Looking for life on Mars - documentary');
|
||||
console.info('Search results:', search);
|
||||
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
|
||||
if (search.videos.length === 0)
|
||||
return console.error('Could not find any video about that on YouTube.');
|
||||
|
||||
const video = await youtube.getDetails(search.videos[0].id).catch((error) => error);
|
||||
console.info('Video details:', video);
|
||||
if (video.error) return;
|
||||
|
||||
if (video instanceof Error)
|
||||
return console.error('Could not get details for ' + search.videos[0].title);
|
||||
|
||||
if (youtube.logged_in) {
|
||||
const myNotifications = await youtube.getNotifications();
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
const Uuid = require('uuid');
|
||||
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You must be signed-in to interact with a video/channel');
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data = {};
|
||||
switch (engagement_type) {
|
||||
case 'like/like':
|
||||
@@ -29,7 +30,7 @@ async function engage(session, engagement_type, args = {}) {
|
||||
data = {
|
||||
context: session.context,
|
||||
commentText: args.text,
|
||||
createCommentParams: Utils.encodeVideoId(args.video_id)
|
||||
createCommentParams: Utils.generateCommentParams(args.video_id)
|
||||
};
|
||||
break;
|
||||
default:
|
||||
@@ -43,15 +44,37 @@ async function engage(session, engagement_type, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function browse(session, action_type) {
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'subscriptions_feed':
|
||||
data = {
|
||||
context: session.context,
|
||||
browseId: 'FEsubscriptions'
|
||||
};
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
async function notifications(session, action_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You must be logged in to fetch notifications');
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'modify_channel_preference':
|
||||
let pref_types = { ALL: 0, NONE: 1, PERSONALIZED: 2 };
|
||||
let pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeChannelId(args.channel_id, pref_types[args.pref.toUpperCase()])
|
||||
params: Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()])
|
||||
};
|
||||
break;
|
||||
case 'get_notification_menu':
|
||||
@@ -68,7 +91,7 @@ async function notifications(session, action_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
return {
|
||||
@@ -78,6 +101,42 @@ async function notifications(session, action_type, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function livechat(session, action_type, args = {}) {
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'live_chat/send_message':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.generateMessageParams(args.channel_id, args.video_id),
|
||||
clientMessageId: `INntLiB${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text: args.text }]
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'live_chat/get_item_context_menu':
|
||||
data = {
|
||||
context: session.context
|
||||
};
|
||||
break;
|
||||
case 'live_chat/moderate':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: args.cmd_params
|
||||
};
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, params: args.params })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
async function getContinuation(session, info = {}) {
|
||||
let data = { context: session.context };
|
||||
|
||||
@@ -97,7 +156,7 @@ async function getContinuation(session, info = {}) {
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -106,4 +165,4 @@ async function getContinuation(session, info = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { engage, notifications, getContinuation };
|
||||
module.exports = { engage, browse, notifications, livechat, getContinuation };
|
||||
@@ -43,6 +43,7 @@ const default_headers = (session) => {
|
||||
const innertube_request_opts = (info) => {
|
||||
if (info.desktop === undefined) info.desktop = true;
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'user-agent': Utils.getRandomUserAgent(info.desktop ? 'desktop' : 'mobile').userAgent,
|
||||
@@ -63,10 +64,6 @@ const innertube_request_opts = (info) => {
|
||||
req_opts.headers.authorization = info.session.cookie.length < 1 ? `Bearer ${info.session.access_token}` : info.session.auth_apisid;
|
||||
}
|
||||
|
||||
if (info.data) {
|
||||
req_opts.headers['content-length'] = Buffer.byteLength(JSON.stringify(info.data), 'utf8');
|
||||
}
|
||||
|
||||
if (info.id) {
|
||||
req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id;
|
||||
}
|
||||
@@ -161,17 +158,16 @@ const formatVideoData = (data, context, desktop) => {
|
||||
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
|
||||
// Actions
|
||||
// Functions
|
||||
video_details.like = () => {};
|
||||
video_details.dislike = () => {};
|
||||
video_details.removeLike = () => {};
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.getComments = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
if (metadata.is_live_content) {
|
||||
video_details.getLivechat = () => {};
|
||||
}
|
||||
video_details.getLivechat = () => {};
|
||||
|
||||
// Additional metadata
|
||||
video_details.metadata = metadata;
|
||||
@@ -179,7 +175,14 @@ const formatVideoData = (data, context, desktop) => {
|
||||
return video_details;
|
||||
};
|
||||
|
||||
const filters = (order) => { // TODO: Refactor this crazy thing
|
||||
|
||||
const base64_alphabet = {
|
||||
normal: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
reverse: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
};
|
||||
|
||||
const filters = (order) => {
|
||||
// TODO: Refactor this with protobuf encoding
|
||||
switch (order) {
|
||||
case 'any,any,relevance':
|
||||
return 'EgIQAQ%3D%3D';
|
||||
@@ -329,4 +332,4 @@ const filters = (order) => { // TODO: Refactor this crazy thing
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, filters };
|
||||
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, base64_alphabet, filters };
|
||||
125
lib/Innertube.js
125
lib/Innertube.js
@@ -5,6 +5,7 @@ const Stream = require('stream');
|
||||
const OAuth = require('./OAuth');
|
||||
const Utils = require('./Utils');
|
||||
const Player = require('./Player');
|
||||
const NToken = require('./NToken');
|
||||
const Actions = require('./Actions');
|
||||
const Livechat = require('./Livechat');
|
||||
const Constants = require('./Constants');
|
||||
@@ -145,6 +146,8 @@ class Innertube extends EventEmitter {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id: id });
|
||||
if (!data_continuation.data.contents.twoColumnWatchNextResults.conversationBar) return;
|
||||
video_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, video_data.metadata.channel_id, id);
|
||||
} else {
|
||||
video_data.getLivechat = () => {};
|
||||
}
|
||||
|
||||
video_data.like = () => Actions.engage(this, 'like/like', { video_id: id });
|
||||
@@ -153,13 +156,102 @@ class Innertube extends EventEmitter {
|
||||
video_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.comment = text => Actions.engage(this, 'comment/create_comment', { video_id: id, text });
|
||||
video_data.getComments = () => this.getComments(id);
|
||||
video_data.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: video_data.metadata.channel_id, pref: pref || 'NONE' });
|
||||
|
||||
return video_data;
|
||||
}
|
||||
|
||||
async getComments(video_id, token) {
|
||||
let comment_section_token;
|
||||
|
||||
if (!token) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id });
|
||||
const item_section_renderer = data_continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer);
|
||||
comment_section_token = item_section_renderer.itemSectionRenderer.contents[0].continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
}
|
||||
|
||||
const response = await Actions.getContinuation(this, { continuation_token: comment_section_token || token });
|
||||
if (!response.success) throw new Error('Could not fetch comment section');
|
||||
|
||||
const comments_section = { comments: [] };
|
||||
!token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A');
|
||||
|
||||
let continuation_token;
|
||||
!token && (continuation_token = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token) ||
|
||||
(continuation_token = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token);
|
||||
|
||||
comments_section.getContinuation = () => this.getComments(video_id, continuation_token);
|
||||
|
||||
let contents;
|
||||
!token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems) ||
|
||||
(contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
contents.forEach((thread) => {
|
||||
if (!thread.commentThreadRenderer) return;
|
||||
const comment = {
|
||||
text: thread.commentThreadRenderer.comment.commentRenderer.contentText.runs.map((t) => t.text).join(' '),
|
||||
author: {
|
||||
name: thread.commentThreadRenderer.comment.commentRenderer.authorText.simpleText,
|
||||
thumbnail: thread.commentThreadRenderer.comment.commentRenderer.authorThumbnail.thumbnails,
|
||||
channel_id: thread.commentThreadRenderer.comment.commentRenderer.authorEndpoint.browseEndpoint.browseId
|
||||
},
|
||||
metadata: {
|
||||
published: thread.commentThreadRenderer.comment.commentRenderer.publishedTimeText.runs[0].text,
|
||||
is_liked: thread.commentThreadRenderer.comment.commentRenderer.isLiked,
|
||||
is_channel_owner: thread.commentThreadRenderer.comment.commentRenderer.authorIsChannelOwner,
|
||||
like_count: thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText,
|
||||
reply_count: thread.commentThreadRenderer.comment.commentRenderer.replyCount || 0,
|
||||
id: thread.commentThreadRenderer.comment.commentRenderer.commentId,
|
||||
}
|
||||
};
|
||||
comments_section.comments.push(comment);
|
||||
});
|
||||
|
||||
return comments_section;
|
||||
}
|
||||
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await Actions.browse(this, 'subscriptions_feed');
|
||||
if (!response.success) throw new Error('Could not fetch subscriptions feed');
|
||||
|
||||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents;
|
||||
const subscriptions_feed = {};
|
||||
|
||||
contents.forEach((section) => {
|
||||
if (!section.itemSectionRenderer) return;
|
||||
|
||||
const section_contents = section.itemSectionRenderer.contents[0];
|
||||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
|
||||
|
||||
const key = section_contents.shelfRenderer.title.runs[0].text;
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')] = [];
|
||||
|
||||
section_items.forEach((item) => {
|
||||
const content = {
|
||||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
id: item.gridVideoRenderer.videoId,
|
||||
channel: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
|
||||
metadata: {
|
||||
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A',
|
||||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails || [],
|
||||
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A',
|
||||
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
|
||||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
|
||||
}
|
||||
};
|
||||
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')].push(content);
|
||||
});
|
||||
});
|
||||
|
||||
return subscriptions_feed;
|
||||
}
|
||||
|
||||
async getNotifications() {
|
||||
const response = await Actions.notifications(this, 'get_notification_menu');
|
||||
if (!response.success) throw new Error('Could not fetch notifications');
|
||||
|
||||
const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0];
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' };
|
||||
return contents.multiPageMenuNotificationSectionRenderer.items.map((notification) => {
|
||||
@@ -180,6 +272,7 @@ class Innertube extends EventEmitter {
|
||||
|
||||
async getUnseenNotificationsCount() {
|
||||
const response = await Actions.notifications(this, 'get_unseen_count');
|
||||
if (!response.success) throw new Error('Could not fetch unseen notifications count');
|
||||
return response.data.unseenCount;
|
||||
}
|
||||
|
||||
@@ -216,12 +309,12 @@ class Innertube extends EventEmitter {
|
||||
format.url = format.url || format.signatureCipher || format.cipher;
|
||||
|
||||
if (format.signatureCipher || format.cipher) {
|
||||
format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player.sig_decipher_sc, this.player.encodeN).decipher();
|
||||
format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player).decipher();
|
||||
} else {
|
||||
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', this.player.encodeN(url_components.searchParams.get('n')));
|
||||
url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n')));
|
||||
format.url = url_components.toString();
|
||||
}
|
||||
|
||||
@@ -281,7 +374,12 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
|
||||
if (options.type == 'videoandaudio') {
|
||||
const response = await Axios.get(selected_format.url, { cancelToken: new CancelToken(function executor(c) { cancel = c; }), responseType: 'stream', reponseEncoding: 'binary', headers: Constants.stream_headers() }).catch((error) => error);
|
||||
const response = await Axios.get(selected_format.url, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.stream_headers()
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) {
|
||||
stream.emit('error', { message: response.message, type: 'REQUEST_FAILED' });
|
||||
return stream;
|
||||
@@ -312,11 +410,19 @@ class Innertube extends EventEmitter {
|
||||
let chunk_start = 0;
|
||||
let chunk_end = chunk_size;
|
||||
let downloaded_size = 0;
|
||||
let end = false;
|
||||
|
||||
stream.emit('start');
|
||||
|
||||
const downloadChunk = async () => {
|
||||
const response = await Axios.get(selected_format.url, { cancelToken: new CancelToken(function executor(c) { cancel = c; }), responseType: 'stream', headers: Constants.stream_headers(`bytes=${chunk_start}-${chunk_end || ''}`) }).catch((error) => error);
|
||||
if (chunk_end >= selected_format.contentLength) end = true;
|
||||
|
||||
const response = await Axios.get(`${selected_format.url}&range=${chunk_start}-${chunk_end || ''}`, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.stream_headers()
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) {
|
||||
stream.emit('error', { message: response.message, type: 'REQUEST_FAILED' });
|
||||
return stream;
|
||||
@@ -338,17 +444,14 @@ class Innertube extends EventEmitter {
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
chunk_start = chunk_end + 1;
|
||||
chunk_end += chunk_size;
|
||||
|
||||
if (downloaded_size < selected_format.contentLength) {
|
||||
if (!end) {
|
||||
chunk_start = chunk_end + 1;
|
||||
chunk_end += chunk_size;
|
||||
downloadChunk();
|
||||
} else {
|
||||
stream.emit('end');
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(stream, { end: false });
|
||||
response.data.pipe(stream, { end });
|
||||
};
|
||||
downloadChunk();
|
||||
}
|
||||
|
||||
107
lib/Livechat.js
107
lib/Livechat.js
@@ -1,10 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Actions = require('./Actions');
|
||||
const Constants = require('./Constants');
|
||||
const EventEmitter = require('events');
|
||||
const Uuid = require("uuid");
|
||||
|
||||
class Livechat extends EventEmitter {
|
||||
constructor(session, token, channel_id, video_id) {
|
||||
@@ -17,65 +16,15 @@ class Livechat extends EventEmitter {
|
||||
this.message_queue = [];
|
||||
this.id_cache = [];
|
||||
|
||||
this.poll_intervals_ms = 0;
|
||||
this.poll_intervals_ms = 1000;
|
||||
this.running = true;
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
async sendMessage(text) {
|
||||
let data = {
|
||||
context: this.session.context,
|
||||
params: Utils.encodeChannelIdWithVideoId(this.channel_id, this.video_id),
|
||||
clientMessageId: `INntLiB${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text }]
|
||||
}
|
||||
};
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/send_message${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.response.data.error.message };
|
||||
|
||||
const deleteMessage = async () => {
|
||||
/*
|
||||
* The first request is made to get the chat options and the delete command endpoint,
|
||||
* these options contain the required params to delete a message (a string composed of clientId, the channelId of the channel you're watching, your public channelId and the id of the message you sent).
|
||||
* All put together with some binary data and then base64ed twice (yes, twice lm*o top notch security).
|
||||
**/
|
||||
const item_menu_res = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_item_context_menu?params=${response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params}&pbj=1${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify({ context: this.session.context }), Constants.innertube_request_opts({ session: this.session, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (item_menu_res instanceof Error) return { success: false, status_code: item_menu_res.response.status, message: item_menu_res.response.data.error.message };
|
||||
const chat_item_menu = item_menu_res.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
const delete_message_reqbody = {
|
||||
context: this.session.context,
|
||||
params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params
|
||||
};
|
||||
|
||||
const delete_message_cmd = await Axios.post(`${Constants.urls.YT_BASE_URL}${chat_item_menu.menuServiceItemRenderer.serviceEndpoint.commandMetadata.webCommandMetadata.apiUrl}${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify(delete_message_reqbody), Constants.innertube_request_opts({ session: this.session, delete_message_reqbody, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (delete_message_cmd instanceof Error) return { success: false, status_code: delete_message_cmd.response.status, message: delete_message_cmd.response.data.error.message };
|
||||
return { success: true, status_code: response.status };
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
deleteMessage: () => deleteMessage(),
|
||||
message_data: {
|
||||
text: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '),
|
||||
author: {
|
||||
name: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/',
|
||||
channel_id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId,
|
||||
profile_picture: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails
|
||||
},
|
||||
timestamp: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec,
|
||||
id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enqueueActionGroup(group) {
|
||||
group.forEach((action) => {
|
||||
if (!action.addChatItemAction) return;
|
||||
if (!action.addChatItemAction) return; //TODO: handle different action types
|
||||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer;
|
||||
if (!message_content) return;
|
||||
|
||||
@@ -113,9 +62,9 @@ class Livechat extends EventEmitter {
|
||||
setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime());
|
||||
this.id_cache.push(message.id);
|
||||
});
|
||||
|
||||
|
||||
this.message_queue = [];
|
||||
|
||||
|
||||
data = { context: this.session.context, videoId: this.video_id };
|
||||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
|
||||
|
||||
@@ -133,15 +82,45 @@ class Livechat extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
// How long we should wait to poll the chat again.
|
||||
if (continuation_contents.liveChatContinuation.continuations[0].timedContinuationData) {
|
||||
this.poll_intervals_ms = continuation_contents.liveChatContinuation.continuations[0].timedContinuationData.timeoutMs;
|
||||
} else {
|
||||
this.poll_intervals_ms = 4000;
|
||||
}
|
||||
this.livechat_poller = setTimeout(async () => await this.poll(), this.poll_intervals_ms);
|
||||
}
|
||||
|
||||
await this.poll();
|
||||
this.livechat_poller = setTimeout(() => this.poll(), this.poll_intervals_ms);
|
||||
async sendMessage(text) {
|
||||
const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
|
||||
if (!message.success) return message;
|
||||
|
||||
const deleteMessage = async () => {
|
||||
const menu = await Actions.livechat(this.session, 'live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
|
||||
if (!menu.success) return menu;
|
||||
|
||||
const chat_item_menu = menu.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
|
||||
if (!cmd.success) return cmd;
|
||||
|
||||
return { success: true, status_code: cmd.status_code };
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: message.status_code,
|
||||
deleteMessage,
|
||||
message_data: {
|
||||
text: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '),
|
||||
author: {
|
||||
name: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/',
|
||||
channel_id: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId,
|
||||
profile_picture: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails
|
||||
},
|
||||
timestamp: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec,
|
||||
id: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async blockUser(msg_params) {
|
||||
/* TODO: Implement this */
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
||||
129
lib/NToken.js
Normal file
129
lib/NToken.js
Normal file
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
|
||||
class NToken {
|
||||
constructor(raw_code) {
|
||||
this.raw_code = raw_code;
|
||||
this.null_placeholder_regex = /c\[(.*?)\]=c/g;
|
||||
this.transformation_args_regex = /c\[(.*?)\]\((.+?)\)/g;
|
||||
}
|
||||
|
||||
transform(n) {
|
||||
let n_token = n.split('');
|
||||
let transformations = this.getTransformationData(this.raw_code);
|
||||
|
||||
// Identifies the necessary transformation functions and emulates them accordingly.
|
||||
transformations = transformations.map((el) => {
|
||||
if (el != null && typeof el != 'number') {
|
||||
const is_reverse_base64 = el.includes('case 65:');
|
||||
if (el.includes('function(d){for(var')) {
|
||||
el = (arr) => this.pushSplice(arr);
|
||||
} else if (el.includes('d.push(e)')) {
|
||||
el = (arr, item) => this.push(arr, item);
|
||||
} else if (el.includes('d.reverse()')) {
|
||||
el = (arr) => this.reverse(arr);
|
||||
} else if (el.includes('d.length;d.splice(e,1)')) {
|
||||
el = (arr, index) => this.spliceOnce(arr, index);
|
||||
} else if (el.includes('d[0])[0])')) {
|
||||
el = (arr, index) => this.spliceTwice(arr, index);
|
||||
} else if (el.includes('reverse().forEach')) {
|
||||
el = (arr, index) => this.spliceReverseUnshift(arr, index);
|
||||
} else if (el.includes('f=d[0];d[0]')) {
|
||||
el = (arr, index) => this.swapFirstItem(arr, index);
|
||||
} else if (el.includes('unshift(d.pop())')) {
|
||||
el = (arr, index) => this.unshiftPop(arr, index);
|
||||
} else if (el.includes('switch')) {
|
||||
el = (arr, e) => this.translateAB(arr, e, is_reverse_base64);
|
||||
} else if (el === 'b') {
|
||||
el = n_token;
|
||||
}
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
// Fills the null placeholders with a copy of the transformations array.
|
||||
let null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1]));
|
||||
null_placeholder_positions.forEach((pos) => transformations[pos] = transformations);
|
||||
|
||||
// Parses and emulates calls to functions of the transformations array.
|
||||
let transformation_args = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_args_regex)].map((params) => ({ index: params[1], params: params[2] }));
|
||||
transformation_args.forEach((data) => {
|
||||
const index = data.index;
|
||||
const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]);
|
||||
transformations[index](transformations[param_index[0]], transformations[param_index[1]]);
|
||||
});
|
||||
|
||||
return n_token.join('');
|
||||
}
|
||||
|
||||
getTransformationData() {
|
||||
let transformation_data = '[' + Utils.getStringBetweenStrings(this.raw_code, 'c=[', '];c') + ']';
|
||||
transformation_data = transformation_data
|
||||
.replace(/function\(d,e\)/g, '"function(d,e)')
|
||||
.replace(/function\(d\)/g, '"function(d)')
|
||||
.replace(/,b,/g, ',"b",')
|
||||
.replace(/,b/g, ',"b"')
|
||||
.replace(/b,/g, '"b",')
|
||||
.replace(/b]/g, '"b"]')
|
||||
.replace(/},/g, '}",')
|
||||
.replace(/""/g, '')
|
||||
.replace(/length]\)}"/g, 'length])}');
|
||||
|
||||
return JSON.parse(transformation_data);
|
||||
}
|
||||
|
||||
translateAB(arr, e, is_reverse_base64) {
|
||||
let characters = is_reverse_base64 && Constants.base64_alphabet.reverse || Constants.base64_alphabet.normal;
|
||||
arr.forEach(function(char, index, loc) {
|
||||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]);
|
||||
}, e.split(''));
|
||||
}
|
||||
|
||||
unshiftPop(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
for (; index--;) {
|
||||
arr.unshift(arr.pop());
|
||||
}
|
||||
}
|
||||
|
||||
swapFirstItem(arr, index) {
|
||||
let oldValue = arr[0];
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr[0] = arr[index];
|
||||
arr[index] = oldValue;
|
||||
}
|
||||
|
||||
spliceReverseUnshift(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(-index).reverse().forEach(function(f) {
|
||||
arr.unshift(f);
|
||||
});
|
||||
}
|
||||
|
||||
spliceOnce(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
|
||||
spliceTwice(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(0, 1, arr.splice(index, 1, arr[0])[0]);
|
||||
}
|
||||
|
||||
pushSplice(arr) {
|
||||
for (let index = arr.length; index;)
|
||||
arr.push(arr.splice(--index, 1)[0]);
|
||||
}
|
||||
|
||||
push(arr, item) {
|
||||
arr.push(item);
|
||||
}
|
||||
|
||||
reverse(arr) {
|
||||
arr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NToken;
|
||||
@@ -36,8 +36,7 @@ class Player {
|
||||
}
|
||||
|
||||
getNEncoder(data) {
|
||||
const raw_code = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");';
|
||||
this.encodeN = Utils.createFunction('a', raw_code);
|
||||
this.ntoken_sc = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const NToken = require('./NToken');
|
||||
const QueryString = require('querystring');
|
||||
|
||||
class SigDecipher {
|
||||
constructor(url, cver, func_code, encode_n) {
|
||||
constructor(url, cver, player) {
|
||||
this.url = url;
|
||||
this.cver = cver;
|
||||
this.func_code = func_code;
|
||||
this.encode_n = encode_n;
|
||||
this.player = player;
|
||||
this.func_regex = /(.{2}):function\(.*?\){(.*?)}/g;
|
||||
this.actions_regex = /;.{2}\.(.{2})\(.*?,(.*?)\)/g;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class SigDecipher {
|
||||
let actions;
|
||||
let signature = args.s.split('');
|
||||
|
||||
while ((actions = this.actions_regex.exec(this.func_code)) !== null) {
|
||||
while ((actions = this.actions_regex.exec(this.player.sig_decipher_sc)) !== null) {
|
||||
switch (actions[1]) {
|
||||
case functions[0]:
|
||||
reverse(signature, actions[2]);
|
||||
@@ -49,10 +49,11 @@ class SigDecipher {
|
||||
}
|
||||
|
||||
const url_components = new URL(args.url);
|
||||
|
||||
args.sp !== undefined ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join(''));
|
||||
url_components.searchParams.set('cver', this.cver);
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
url_components.searchParams.set('n', this.encode_n(url_components.searchParams.get('n')));
|
||||
url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n')));
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ class SigDecipher {
|
||||
let func;
|
||||
let func_name = [];
|
||||
|
||||
while ((func = this.func_regex.exec(this.func_code)) !== null) {
|
||||
while ((func = this.func_regex.exec(this.player.sig_decipher_sc)) !== null) {
|
||||
if (func[0].includes('reverse()')) {
|
||||
func_name[0] = func[1];
|
||||
} else if (func[0].includes('splice')) {
|
||||
|
||||
69
lib/Utils.js
69
lib/Utils.js
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Proto = require('protons');
|
||||
const Crypto = require('crypto');
|
||||
const UserAgent = require('user-agents');
|
||||
|
||||
@@ -35,37 +37,50 @@ function escapeStringRegexp(string) {
|
||||
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
|
||||
}
|
||||
|
||||
function createFunction(input, raw_code) { // I hate this
|
||||
return new Function(input, raw_code);
|
||||
function encodeNotificationPref(channel_id, index) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.NotificationPreferences.encode({
|
||||
channel_id,
|
||||
pref_id: {
|
||||
index
|
||||
},
|
||||
number_0: 0,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
function encodeVideoId(id) {
|
||||
return encodeURIComponent(`${Buffer.from(`` + id + `*`).toString('base64').slice(0, -1)}BQBw==`);
|
||||
function generateMessageParams(channel_id, video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.LiveMessageParams.encode({
|
||||
params: {
|
||||
ids: {
|
||||
channel_id,
|
||||
video_id
|
||||
}
|
||||
},
|
||||
number_0: 1,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
function encodeChannelId(id, notification_pref) {
|
||||
const buff_start = `
|
||||
`;
|
||||
const buff_end = [
|
||||
``, // all
|
||||
``, // none
|
||||
``, // personalized
|
||||
];
|
||||
function generateCommentParams(video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
|
||||
|
||||
let encodedId = Buffer.from([buff_start, id, buff_end[notification_pref]].join('')).toString('base64');
|
||||
return encodeURIComponent(`${encodedId}GAAgBA==`);
|
||||
const buf = youtube_proto.CreateCommentParams.encode({
|
||||
video_id,
|
||||
params: {
|
||||
index: 0
|
||||
},
|
||||
number: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
function encodeChannelIdWithVideoId(channel_id, video_id) {
|
||||
const buff_start = `
|
||||
)*'
|
||||
`;
|
||||
const buff_middle = ``;
|
||||
const buff_end = ``;
|
||||
|
||||
// Yes, we also have to base64 these twice lol
|
||||
let encodedIds = Buffer.from([buff_start, channel_id, buff_middle, video_id, buff_end].join('')).toString('base64');
|
||||
return `${Buffer.from(encodedIds).toString('base64').slice(0, -4)}JTNE`;
|
||||
}
|
||||
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeChannelIdWithVideoId, encodeVideoId, encodeChannelId };
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref };
|
||||
34
lib/proto/youtube.proto
Normal file
34
lib/proto/youtube.proto
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
101
package-lock.json
generated
101
package-lock.json
generated
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"protons": "^2.0.3",
|
||||
"time-to-seconds": "^1.1.5",
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -76,6 +77,35 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"node_modules/multiformats": {
|
||||
"version": "9.4.9",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.9.tgz",
|
||||
"integrity": "sha512-zA84TTJcRfRMpjvYqy63piBbSEdqlIGqNNSpP6kspqtougqjo60PRhIFo+oAxrjkof14WMCImvr7acK6rPpXLw=="
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"node_modules/protons": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
|
||||
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1",
|
||||
"signed-varint": "^2.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/signed-varint": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
|
||||
"integrity": "sha1-UKmYnafJjCxh2tEZvJdHDvhSgSk=",
|
||||
"dependencies": {
|
||||
"varint": "~5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/time-to-seconds": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/time-to-seconds/-/time-to-seconds-1.1.5.tgz",
|
||||
@@ -85,6 +115,14 @@
|
||||
"vscode": "^1.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"dependencies": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
|
||||
@@ -99,9 +137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.0.805",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.805.tgz",
|
||||
"integrity": "sha512-asL9HOjdiJw+A7T7rYnxFC3wAt3u7Shle8fWHECKoqFV2JqI21Xoh8i29cdIsWP1Tn8FZcB0doUQ0h1pUwLkxw==",
|
||||
"version": "1.0.814",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.814.tgz",
|
||||
"integrity": "sha512-OU3lCkkaUCghnZiZuN2edSJ+//ituJihs3P1FCjCDwe1tMBS8eni68CQ0TBbPoVPDtoAqxM3Z7qbP7EWGlhBxA==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -114,6 +152,11 @@
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -155,11 +198,48 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"multiformats": {
|
||||
"version": "9.4.9",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.9.tgz",
|
||||
"integrity": "sha512-zA84TTJcRfRMpjvYqy63piBbSEdqlIGqNNSpP6kspqtougqjo60PRhIFo+oAxrjkof14WMCImvr7acK6rPpXLw=="
|
||||
},
|
||||
"protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"protons": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
|
||||
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
|
||||
"requires": {
|
||||
"protocol-buffers-schema": "^3.3.1",
|
||||
"signed-varint": "^2.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"signed-varint": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
|
||||
"integrity": "sha1-UKmYnafJjCxh2tEZvJdHDvhSgSk=",
|
||||
"requires": {
|
||||
"varint": "~5.0.0"
|
||||
}
|
||||
},
|
||||
"time-to-seconds": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/time-to-seconds/-/time-to-seconds-1.1.5.tgz",
|
||||
"integrity": "sha512-mpzJDHGF4VdhiahyusCUSy+BWJdN3q8Cluzfy0n7GMU9IIj+HJDX9bbbr7wVSUiqmRn1vqhhfECgdfj+SByu2A=="
|
||||
},
|
||||
"uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"requires": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
|
||||
@@ -174,9 +254,9 @@
|
||||
}
|
||||
},
|
||||
"user-agents": {
|
||||
"version": "1.0.805",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.805.tgz",
|
||||
"integrity": "sha512-asL9HOjdiJw+A7T7rYnxFC3wAt3u7Shle8fWHECKoqFV2JqI21Xoh8i29cdIsWP1Tn8FZcB0doUQ0h1pUwLkxw==",
|
||||
"version": "1.0.814",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.814.tgz",
|
||||
"integrity": "sha512-OU3lCkkaUCghnZiZuN2edSJ+//ituJihs3P1FCjCDwe1tMBS8eni68CQ0TBbPoVPDtoAqxM3Z7qbP7EWGlhBxA==",
|
||||
"requires": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -186,6 +266,11 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"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": {
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"protons": "^2.0.3",
|
||||
"time-to-seconds": "^1.1.5",
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -33,8 +34,9 @@
|
||||
"like",
|
||||
"dislike",
|
||||
"comment",
|
||||
"downloader",
|
||||
"automation",
|
||||
"downloader",
|
||||
"comments-section",
|
||||
"youtube-downloader"
|
||||
],
|
||||
"bugs": {
|
||||
|
||||
Reference in New Issue
Block a user