Compare commits

...

77 Commits

Author SHA1 Message Date
LuanRT
aeff0c3fdc build (package): increment version 2021-11-19 13:50:50 -03:00
LuanRT
00d67ed417 chore (OAuth): better & simpler regular expression 2021-11-19 13:29:02 -03:00
LuanRT
78f93c7118 fix: add “g” flag so it matches all possible strings 2021-11-19 13:27:07 -03:00
LuanRT
6db3f0ad91 fix: download not possible due to visitorData being undefined 2021-11-14 12:22:56 -03:00
UnbreakCode
cf48385f72 fixed x-goog-visitor-id for downloader 2021-11-14 15:46:21 +01:00
LuanRT
e70eab2416 build (package): increment version 2021-11-13 01:18:17 -03:00
LuanRT
771c6050c4 fix (n-token): yet again YouTube added new functions that do exactly the same thing as before but in a different way 2021-11-13 01:04:03 -03:00
LuanRT
5670228a4f docs: remove licence scan badge, it's not really necessary for this project 2021-11-05 09:12:47 -03:00
LuanRT
62ae384f27 docs: add license scan report and status 2021-11-05 08:42:15 -03:00
fossabot
185cdbd6ce Add license scan report and status
Signed off by: fossabot <badges@fossa.com>
2021-11-05 04:27:51 -07:00
LuanRT
5dd6ef9e24 fix: don't limit range end 2021-11-04 08:01:03 -03:00
LuanRT
309942090d docs: add info event response example 2021-11-04 04:27:35 -03:00
LuanRT
af4a4b8b82 . 2021-11-04 04:13:50 -03:00
LuanRT
b9c9d40077 feat: add support for custom data range 2021-11-04 04:05:16 -03:00
LuanRT
62fbc166c5 chore: format code 2021-11-03 23:34:58 -03:00
LuanRT
c3991dda32 fix (OAuth): forgot to change some variables to uppercase 2021-11-03 23:33:34 -03:00
LuanRT
95e804e8ea docs: fix typos 2021-11-03 21:41:55 -03:00
LuanRT
67a8435421 tests: throw an error if one or more tests fail 2021-11-02 16:41:45 -03:00
LuanRT
1847558d50 test: rename some vars & fix typos 2021-11-02 08:45:18 -03:00
LuanRT
bde915bce3 chore: remove comment on workflow file 2021-11-02 08:03:04 -03:00
LuanRT
a9ad3a31b5 chore: update workflow 2021-11-02 08:01:31 -03:00
LuanRT
e52e6138bd chore: format & fix typos 2021-11-02 07:57:33 -03:00
LuanRT
76248ad143 build (package): update test script 2021-11-02 07:51:36 -03:00
LuanRT
94f441a4e2 chore: format code 2021-11-02 07:45:16 -03:00
LuanRT
685e14fcc1 perf: move to object literal and simplify transformation functions 2021-11-02 07:40:31 -03:00
LuanRT
3cd115461f refactor: change constants to uppercase and refactor some code 2021-11-02 07:37:28 -03:00
LuanRT
6da4ee8fd4 chore: add tests 2021-11-02 07:34:47 -03:00
LuanRT
b095044baa build (package): increment version 2021-10-29 22:22:53 -03:00
LuanRT
ba2b757fdb fix (OAuth): remove any new lines so the client identity can be found more easily 2021-10-29 22:10:43 -03:00
LuanRT
9d7d0d83e1 fix: catch any possible errors when transforming the n token 2021-10-28 22:52:43 -03:00
LuanRT
b893e46634 chore: fix typo 2021-10-25 18:14:35 -03:00
LuanRT
d8ab6f3887 fix: handle errors when initializing 2021-10-25 18:04:28 -03:00
LuanRT
eea5ebfd04 fix: handle errors when initializing 2021-10-25 18:03:57 -03:00
LuanRT
b2117f11b9 chore: add comments and format code 2021-10-25 18:02:28 -03:00
LuanRT
389b0f362f chore: format code 2021-10-25 02:25:52 -03:00
LuanRT
6ce4a89766 build (package): increment version 2021-10-25 02:24:12 -03:00
LuanRT
4d7573c46f docs: rephrasing 2021-10-25 01:25:00 -03:00
LuanRT
445de3546d chore: update examples 2021-10-25 01:23:07 -03:00
LuanRT
3b265119d6 docs: rephrase 2021-10-25 01:20:00 -03:00
LuanRT
933ec50a1c build (package): increment version 2021-10-24 23:36:12 -03:00
LuanRT
922ab51a8d build (package): increment version 2021-10-24 23:31:19 -03:00
LuanRT
330943cbc0 docs: add examples for subscriptions feed & comments 2021-10-24 23:25:51 -03:00
LuanRT
368dfc4ea3 format: remove unused param 2021-10-24 17:07:14 -03:00
LuanRT
3a9d8a411e refactor: descramble n token algorithm instead of executing it directly for better security 2021-10-24 17:03:09 -03:00
LuanRT
47ea630329 feat: now it's possible to fetch the comments section 2021-10-23 15:48:01 -03:00
LuanRT
c8e8f34a83 refactor: change pref index order 2021-10-22 17:37:18 -03:00
LuanRT
14a5b0f0e8 chore: improve protobuf encoding 2021-10-22 17:33:47 -03:00
LuanRT
e817eb46d4 chore: remove unnecessary keyword from proto file 2021-10-22 04:42:48 -03:00
LuanRT
6ca6e22fea chore: update examples 2021-10-22 04:38:33 -03:00
LuanRT
d11d03bd92 chore: update workflow 2021-10-22 04:36:37 -03:00
LuanRT
27962242b7 dev: encode with actual an actual protobuf lib instead of hard-coding stuff 2021-10-22 04:32:51 -03:00
LuanRT
f54993e3b7 build (package): add protons dependency 2021-10-22 02:47:18 -03:00
LuanRT
d2ec5ebe9c dev: add proto file 2021-10-22 02:38:59 -03:00
LuanRT
10abf386b4 revert: put back accidentally removed code 2021-10-20 02:53:32 -03:00
LuanRT
f0360cac69 format: rephrase comments 2021-10-20 02:51:21 -03:00
LuanRT
24c11f2c06 fix: remove unnecessary chat polling call 2021-10-20 02:47:06 -03:00
LuanRT
ff2ca5ad3b format: remove unnecessary async keyword 2021-10-20 02:21:05 -03:00
LuanRT
391a4300c1 refactor: organize live chat actions 2021-10-20 02:17:13 -03:00
LuanRT
530e310da6 docs: update live chat example 2021-10-17 02:42:39 -03:00
LuanRT
73bac32886 fix: don't make additional requests if there aren't any more chunks to download 2021-10-17 02:37:35 -03:00
LuanRT
8023e74adb build (package): increment version 2021-10-15 03:32:05 -03:00
LuanRT
09ce31061d docs: add live chat examples & improve the rest of the documentation 2021-10-15 03:29:47 -03:00
LuanRT
7072485782 format: organize code & fix accidental typos 2021-10-15 00:39:29 -03:00
LuanRT
4bd79e5903 fix(livechat): clear message queue once its contents are no longer necessary 2021-10-15 00:31:53 -03:00
LuanRT
028e723226 format: rephrasing error messages 2021-10-15 00:09:14 -03:00
LuanRT
1a68875aad fix: add some better error handling 2021-10-15 00:04:39 -03:00
LuanRT
de7d52a62c fix: don't try polling the livechat if the live has already ended 2021-10-14 23:56:37 -03:00
LuanRT
b8a2fd01cc chore (examples): remove cookies example 2021-10-14 19:56:20 -03:00
LuanRT
b9ea6e36c8 fix: streams with both audio and video should always emit 'end' 2021-10-14 19:13:22 -03:00
LuanRT
783e6d2435 format: rephrasing comment 2021-10-14 17:11:19 -03:00
LuanRT
0b11e441ff chore: final adjustments 2021-10-14 17:06:20 -03:00
LuanRT
d7db0d2304 style: format code and remove unused stuff 2021-10-14 16:55:35 -03:00
LuanRT
d674eef530 feat: add support for livechats 2021-10-14 16:54:02 -03:00
LuanRT
17aa4edb66 refactor: remove unused parameter in the Innertube constructor 2021-10-13 21:54:44 -03:00
LuanRT
656c0fb7b8 fix: just a typo 2021-10-13 15:23:49 -03:00
LuanRT
d4dce16be0 feat: add method to change notification preferences and get unseen notifications count 2021-10-13 05:55:02 -03:00
LuanRT
80a9ece314 format: remove unnecessary async keyword 2021-10-12 21:08:08 -03:00
17 changed files with 1645 additions and 752 deletions

View File

@@ -1,6 +1,5 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# TODO: Make actual tests instead of just running the examples file
name: Build
@@ -17,7 +16,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:
@@ -27,4 +26,4 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: node ./examples
- run: npm test

471
README.md
View File

@@ -1,22 +1,30 @@
# YouTube.js
[![Build](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg)](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml)
[![NPM](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)](https://www.npmjs.com/package/youtubei.js)
[![CodeFactor](https://www.codefactor.io/repository/github/luanrt/youtube.js/badge)](https://www.codefactor.io/repository/github/luanrt/youtube.js)
An object-oriented wrapper around the Innertube API, which is what YouTube itself uses. This makes YouTube.js fast, simple & efficient. And big thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for his research on the workings of the Innertube API!
#### 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.
- Fetch notifications (sign-in required).
- Subscribe/Unsubscribe/Like/Dislike/Comment (sign-in required).
- 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, etc
- 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, such as commenting, subscribing, sending messages to a live chat, etc.
#### Do I need an API key to use this?
No, since it's basically what YouTube itself uses to populate its app/website no API keys are required.
No, YouTube.js does not use any official API so no API keys are required.
## Installation
@@ -30,11 +38,13 @@ npm install youtubei.js
[2. Interactions](https://github.com/LuanRT/YouTube.js#interactions)
[3. Downloading Videos](https://github.com/LuanRT/YouTube.js#downloading-videos)
[3. Fetching live chats](https://github.com/LuanRT/YouTube.js#fetching-live-chats)
[4. Signing-in](https://github.com/LuanRT/YouTube.js#signing-in)
[4. Downloading videos](https://github.com/LuanRT/YouTube.js#downloading-videos)
[5. Disclaimer](https://github.com/LuanRT/YouTube.js#disclaimer)
[5. Signing-in](https://github.com/LuanRT/YouTube.js#signing-in)
[6. Disclaimer](https://github.com/LuanRT/YouTube.js#disclaimer)
First of all we're gonna start by initializing the Innertube class:
@@ -118,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);
@@ -178,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();
@@ -220,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
@@ -228,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
@@ -237,13 +510,126 @@ 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!');
```
All of the interactions above will return ```{ success: true, status_code: 200 }``` if everything goes alright.
* Change notification preferences:
```js
const video = await youtube.getDetails(VIDEO_ID_HERE);
// Can be: ALL | NONE | PERSONALIZED
await video.setNotificationPref('ALL');
```
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 plus you can also send messages!
```js
const Innertube = require('youtubei.js');
async function start() {
const youtube = await new Innertube();
const search = await youtube.search('Some random livestream');
const video = await youtube.getDetails(search.videos[0].id);
// This should only be called if you're sure it's a livestream and that it's still ongoing
const livechat = video.getLivechat();
// Updated stats about the livestream
livechat.on('update-metadata', (data) => {
console.info('Info:', data);
});
// Fired whenever there is a new message or other chat events
livechat.on('chat-update', (message) => {
console.info(`- ${message.author.name}\n${message.text}\n\n`);
if(message.text == '!info') {
livechat.sendMessage('Hello! This message was sent from YouTube.js');
}
});
}
start();
```
Stop fetching the live chat:
```js
livechat.stop();
```
Deleting a message:
```js
const msg = await livechat.sendMessage('Nice livestream!');
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');
async function start() {
const youtube = await new Innertube();
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
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”
});
stream.pipe(fs.createWriteStream(`./${search.videos[0].title}.mp4`));
stream.on('start', () => {
console.info('[DOWNLOADER]', 'Starting download now!');
});
stream.on('info', (info) => {
// { video_details: {..}, selected_format: {..}, formats: {..} }
console.info('[DOWNLOADER]', `Downloading ${info.video_details.title} by ${info.video_details.metadata.channel_name}`);
});
stream.on('progress', (info) => {
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(`[DOWNLOADER] Downloaded ${info.percentage}% (${info.downloaded_size}MB) of ${info.size}MB`);
});
stream.on('end', () => {
process.stdout.clearLine();
process.stdout.cursorTo(0);
console.info('[DOWNLOADER]', 'Done!');
});
stream.on('error', (err) => console.error('[ERROR]', err));
}
start();
```
You can also download only a portion of a video by specifying a range:
```js
const stream = youtube.download(VIDEO_ID, {
//...
type: 'videoandaudio',
range: { start: 0, end: 1048576 * 5 }
});
```
Cancelling a download:
```js
stream.cancel();
```
### Signing-in:
---
@@ -253,12 +639,12 @@ 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');
const Innertube = require('youtubei.js');
const creds_path = './yt_oauth_creds.json';
const creds_path = './yt_oauth_creds.json';
async function start() {
const creds = fs.existsSync(creds_path) && JSON.parse(fs.readFileSync(creds_path).toString()) || {};
@@ -301,62 +687,19 @@ async function start() {
start();
```
### Downloading videos:
```js
const fs = require('fs');
const Innertube = require('youtubei.js');
async function start() {
const youtube = await new Innertube();
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
quality: '360p', // if a video doesn't have a specific quality it'll fall back to 360p, this is ignored when type is set to audio
type: 'videoandaudio' // can be “video”, “audio” and “videoandaudio”
});
stream.pipe(fs.createWriteStream(`./${search.videos[0].title}.mp4`));
stream.on('start', () => {
console.info('[DOWNLOADER]', 'Starting download now!');
});
stream.on('info', (info) => {
console.info('[DOWNLOADER]', `Downloading ${info.video_details.title} by ${info.video_details.metadata.channel_name}`);
});
stream.on('progress', (info) => {
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(`[DOWNLOADER] Downloaded ${info.percentage}% (${info.downloaded_size}MB) of ${info.size}MB`);
});
stream.on('end', () => {
process.stdout.clearLine();
process.stdout.cursorTo(0);
console.info('[DOWNLOADER]', 'Done!');
});
stream.on('error', (err) => console.error('[ERROR]', err));
}
start();
```
Cancelling a download:
```js
stream.cancel();
```
## Note
Never sign-in with your personal account, you might get banned if you spam (don't ever do that) or simply because YouTube detected unusual activity coming from your account. Also, I'm not responsible if any of that happens to you.
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
## Disclaimer
This project is not affiliated with, endorsed, or sponsored by YouTube or any of their affiliates or subsidiaries.
All trademarks, logos and brand names are the property of their respective owners.
Should you have any questions or concerns please contact me directly via email.
## License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@@ -1,17 +1,24 @@
'use strict';
const fs = require('fs');
const Innertube = require('..');
const COOKIE = 'YT_COOKIE_HERE';
async function start() {
const youtube = await new Innertube(COOKIE);
const youtube = await new Innertube();
// 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 instanceof Error)
return console.error('Could not get details for ' + search.videos[0].title);
if (youtube.logged_in) {
const myNotifications = await youtube.getNotifications();
console.info('My notifications:', myNotifications);
@@ -54,7 +61,7 @@ async function start() {
type: 'videoandaudio' // can be “video”, “audio” and “videoandaudio”
});
stream.pipe(fs.createWriteStream(`./${search.videos[0].title}.mp4`));
stream.pipe(fs.createWriteStream(`./${search.videos[0].id}.mp4`));
stream.on('start', () => {
console.info('[DOWNLOADER]', 'Starting download now!');

View File

@@ -1,171 +1,165 @@
'use strict';
const Uuid = require('uuid');
const Axios = require('axios');
const Utils = require('./Utils');
const Constants = require('./Constants');
async function subscribe(session, video_id, channel_id) {
if (!session.logged_in) throw new Error('You must be logged in to subscribe to a channel');
let data = {
context: session.context,
channelIds: [channel_id]
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/subscription/subscribe${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
};
async function engage(session, engagement_type, args = {}) {
if (!session.logged_in) throw new Error('You are not logged in');
let data = {};
switch (engagement_type) {
case 'like/like':
case 'like/dislike':
case 'like/removelike':
data = {
context: session.context,
target: {
videoId: args.video_id
}
};
break;
case 'subscription/subscribe':
case 'subscription/unsubscribe':
data = {
context: session.context,
channelIds: [args.channel_id]
};
break;
case 'comment/create_comment':
data = {
context: session.context,
commentText: args.text,
createCommentParams: Utils.generateCommentParams(args.video_id)
};
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);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
success: true,
status_code: response.status
};
}
async function unsubscribe(session, video_id, channel_id) {
if (!session.logged_in) throw new Error('You must be logged in to unsubscribe from a channel');
let data = {
context: session.context,
channelIds: [channel_id]
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/subscription/unsubscribe${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
};
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_REQOPTS({ 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 likeVideo(session, video_id) {
if (!session.logged_in) throw new Error('You must be logged in to like a video');
let data = {
context: session.context,
target: {
videoId: video_id
}
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/like${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
};
async function notifications(session, action_type, args = {}) {
if (!session.logged_in) throw new Error('You are not logged in');
let 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()])
};
break;
case 'get_notification_menu':
data = {
context: session.context,
notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'
};
break;
case 'get_unseen_count':
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);
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 {
success: true,
status_code: response.status,
data: response.data
};
}
async function dislikeVideo(session, video_id) {
if (!session.logged_in) throw new Error('You must be logged in to like a video');
let data = {
context: session.context,
target: {
videoId: video_id
}
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/dislike${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
};
async function livechat(session, action_type, args = {}) {
let data;
switch (action_type) {
case 'live_chat/send_message':
data = {
context: session.context,
params: Utils.generateMessageParams(args.channel_id, args.video_id),
clientMessageId: `ytjs-${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_REQOPTS({ 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 removeLike(session, video_id) {
if (!session.logged_in) throw new Error('You must be logged in to remove a like/dislike.');
let data = {
context: session.context,
target: {
videoId: video_id
}
};
async function getContinuation(session, info = {}) {
let data = { context: session.context };
info.continuation_token && (data.continuation = info.continuation_token);
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/removelike${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
if (info.video_id) {
data.videoId = info.video_id;
data.racyCheckOk = true;
data.contentCheckOk = false;
data.autonavState = 'STATE_NONE';
data.playbackContext = {
vis: 0,
lactMilliseconds: '-1'
};
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_REQOPTS({ 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 commentVideo(session, video_id, text) {
if (!text) throw new Error('No text was provided');
if (!session.logged_in) throw new Error('You must be logged in to post a comment.');
let data = {
context: session.context,
commentText: text,
createCommentParams: Utils.encodeId(video_id)
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/comment/create_comment${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else if (response.data.responseContext) {
return {
success: true,
status_code: response.status,
};
}
}
async function getNotifications(session) {
if (!session.logged_in) throw new Error('You must be logged in to fetch notifications');
let data = {
context: session.context,
notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'
};
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/get_notification_menu${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error);
if (response instanceof Error) {
return {
success: false,
status_code: response.response.status,
message: response.message
};
} else {
return {
success: true,
status_code: response.status,
data: response.data
};
}
}
module.exports = { subscribe, unsubscribe, likeVideo, dislikeVideo, removeLike, commentVideo, getNotifications };
module.exports = { engage, browse, notifications, livechat, getContinuation };

View File

@@ -2,327 +2,265 @@
const Utils = require('./Utils');
const urls = {
YT_BASE_URL: 'https://www.youtube.com',
YT_MOBILE_URL: 'https://m.youtube.com',
YT_WATCH_PAGE: 'https://m.youtube.com/watch',
};
const oauth = {
scope: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
grant_type: 'http://oauth.net/grant_type/device/1.0',
model_name: 'ytlr::'
};
const oauth_reqopts = {
headers: {
'accept': '*/*',
'origin': urls.YT_BASE_URL,
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
'content-type': 'application/json',
'x-requested-with': 'mark.via.gp',
'referer': `${urls.YT_BASE_URL}/tv`,
'accept-language': 'en-US'
}
};
const default_headers = (session) => {
return {
headers: {
'Cookie': session.cookie,
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
'Referer': 'https://www.google.com/',
'Accept': 'text/html',
'Accept-Language': 'en-US,en',
'Accept-Encoding': 'gzip',
'Upgrade-Insecure-Requests': 1
}
};
};
const innertube_request_opts = (info) => {
if (info.desktop === undefined) info.desktop = true;
let req_opts = {
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': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL,
'origin': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL,
}
};
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.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;
}
return req_opts;
};
const video_details_reqbody = (id, sts, context) => {
return {
playbackContext: {
contentPlaybackContext: {
'currentUrl': '/watch?v=' + id,
'vis': 0,
'splay': false,
'autoCaptionsDefaultOn': false,
'autonavState': 'STATE_OFF',
'html5Preference': 'HTML5_PREF_WANTS',
'signatureTimestamp': sts,
'referer': urls.YT_BASE_URL,
'lactMilliseconds': '-1'
module.exports = {
URLS: {
YT_BASE_URL: 'https://www.youtube.com',
YT_MOBILE_URL: 'https://m.youtube.com',
YT_WATCH_PAGE: 'https://m.youtube.com/watch'
},
OAUTH: {
SCOPE: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
GRANT_TYPE: 'http://oauth.net/grant_type/device/1.0',
MODEL_NAME: 'ytlr::',
HEADERS: {
headers: {
'accept': '*/*',
'origin': 'https://www.youtube.com',
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
'content-type': 'application/json',
'referer': `https://www.youtube.com/tv`,
'accept-language': 'en-US'
}
},
context: context,
videoId: id
};
};
const stream_headers = (range) => {
let headers = {
}
},
DEFAULT_HEADERS: (session) => {
return {
headers: {
'Cookie': session.cookie,
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
'Referer': 'https://www.google.com/',
'Accept': 'text/html',
'Accept-Language': 'en-US,en',
'Accept-Encoding': 'gzip',
'Upgrade-Insecure-Requests': 1
}
};
},
STREAM_HEADERS: {
'Accept': '*/*',
'User-Agent': Utils.getRandomUserAgent('desktop').userAgent,
'Connection': 'keep-alive',
'Origin': urls.YT_BASE_URL,
'Referer': urls.YT_BASE_URL,
'Origin': 'https://www.youtube.com',
'Referer': 'https://www.youtube.com',
'DNT': '?1'
};
if (range) {
headers.Range = range;
},
INNERTUBE_REQOPTS: (info) => {
info.desktop === undefined && (info.desktop = true);
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': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
'origin': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
}
};
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;
}
return req_opts;
},
VIDEO_INFO_REQBODY: (id, sts, context) => {
return {
playbackContext: {
contentPlaybackContext: {
'currentUrl': '/watch?v=' + id,
'vis': 0,
'splay': false,
'autoCaptionsDefaultOn': false,
'autonavState': 'STATE_OFF',
'html5Preference': 'HTML5_PREF_WANTS',
'signatureTimestamp': sts,
'referer': 'https://www.youtube.com',
'lactMilliseconds': '-1'
}
},
context: context,
videoId: id
};
},
BASE64_DIALECT: {
NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
REVERSE: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
},
FUNCS_REGEX: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var h=f|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/,
FUNCS: {
PUSH: 'd.push(e)',
REVERSE_1: 'd.reverse()',
REVERSE_2: 'function(d){for(var',
SPLICE: 'd.length;d.splice(e,1)',
SWAP0_1: 'd[0])[0])',
SWAP0_2: 'f=d[0];d[0]',
ROTATE_1: 'reverse().forEach',
ROTATE_2: 'unshift(d.pop())',
BASE64_DIA: 'function(){for(var',
TRANSLATE_1: 'function(d,e){for(var f',
TRANSLATE_2: 'function(d,e,f){var h=f'
},
// Helper functions, felt like Utils.js wasn't the right place for them:
formatNTransformData: (data) => {
return data
.replace(/function\(d,e\)/g, '"function(d,e)').replace(/function\(d\)/g, '"function(d)')
.replace(/function\(\)/g, '"function()').replace(/function\(d,e,f\)/g, '"function(d,e,f)')
.replace(/\[function\(d,e,f\)/g, '["function(d,e,f)')
.replace(/,b,/g, ',"b",').replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]')
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",').replace(/""/g, '')
.replace(/length]\)}"/g, 'length])}');
},
formatVideoData: (data, context, is_desktop) => {
let video_details = {};
let metadata = {};
if (is_desktop) {
metadata.embed = data.microformat.playerMicroformatRenderer.embed;
metadata.view_count = parseInt(data.videoDetails.viewCount);
metadata.average_rating = data.videoDetails.averageRating;
metadata.length_seconds = data.microformat.playerMicroformatRenderer.lengthSeconds;
metadata.channel_id = data.videoDetails.channelId;
metadata.channel_url = data.microformat.playerMicroformatRenderer.ownerProfileUrl;
metadata.external_channel_id = data.microformat.playerMicroformatRenderer.externalChannelId;
metadata.is_live_content = data.videoDetails.isLiveContent;
metadata.is_family_safe = data.microformat.playerMicroformatRenderer.isFamilySafe;
metadata.is_unlisted = data.microformat.playerMicroformatRenderer.isUnlisted;
metadata.is_private = data.videoDetails.isPrivate;
metadata.has_ypc_metadata = data.microformat.playerMicroformatRenderer.hasYpcMetadata;
metadata.category = data.microformat.playerMicroformatRenderer.category;
metadata.channel_name = data.microformat.playerMicroformatRenderer.ownerChannelName;
metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate || 'N/A';
metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate || 'N/A';
metadata.keywords = data.videoDetails.keywords || [];
video_details.title = data.videoDetails.title;
video_details.description = data.videoDetails.shortDescription;
video_details.thumbnail = data.videoDetails.thumbnail.thumbnails.slice(-1)[0];
video_details.metadata = metadata;
} else {
metadata.embed = data[2].playerResponse.microformat.playerMicroformatRenderer.embed;
metadata.likes = parseInt(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, ''));
metadata.dislikes = parseInt(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, ''));
metadata.view_count = parseInt(data[2].playerResponse.videoDetails.viewCount);
metadata.average_rating = data[2].playerResponse.videoDetails.averageRating;
metadata.length_seconds = data[2].playerResponse.microformat.playerMicroformatRenderer.lengthSeconds;
metadata.channel_id = data[2].playerResponse.videoDetails.channelId;
metadata.channel_url = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerProfileUrl;
metadata.external_channel_id = data[2].playerResponse.microformat.playerMicroformatRenderer.externalChannelId;
metadata.is_live_content = data[2].playerResponse.videoDetails.isLiveContent;
metadata.is_family_safe = data[2].playerResponse.microformat.playerMicroformatRenderer.isFamilySafe;
metadata.is_unlisted = data[2].playerResponse.microformat.playerMicroformatRenderer.isUnlisted;
metadata.is_private = data[2].playerResponse.videoDetails.isPrivate;
metadata.has_ypc_metadata = data[2].playerResponse.microformat.playerMicroformatRenderer.hasYpcMetadata;
metadata.category = data[2].playerResponse.microformat.playerMicroformatRenderer.category;
metadata.channel_name = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerChannelName;
metadata.publish_date = data[2].playerResponse.microformat.playerMicroformatRenderer.publishDate;
metadata.upload_date = data[2].playerResponse.microformat.playerMicroformatRenderer.uploadDate;
metadata.keywords = data[2].playerResponse.videoDetails.keywords;
video_details.title = data[2].playerResponse.videoDetails.title;
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
// 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 = () => {};
video_details.getLivechat = () => {};
// Additional metadata
video_details.metadata = metadata;
}
return video_details;
},
filters: (order) => {
return (({
'any,any,relevance': 'EgIQAQ%3D%3D',
'hour,any,relevance': 'EgIIAQ%3D%3D',
'day,any,relevance': 'EgQIAhAB',
'week,any,relevance': 'EgQIAxAB',
'month,any,relevance': 'EgQIBBAB',
'year,any,relevance': 'EgQIBRAB',
'any,short,relevance': 'EgQQARgB',
'hour,short,relevance': 'EgYIARABGAE%3D',
'day,short,relevance': 'EgYIAhABGAE%3D',
'week,short,relevance': 'EgYIAxABGAE%3D',
'month,short,relevance': 'EgYIBBABGAE%3D',
'year,short,relevance': 'EgYIBRABGAE%3D',
'any,long,relevance': 'EgQQARgC',
'hour,long,relevance': 'EgYIARABGAI%3D',
'day,long,relevance': 'EgYIAhABGAI%3D',
'week,long,relevance': 'EgYIAxABGAI%3D',
'month,long,relevance': 'EgYIBBABGAI%3D',
'year,long,relevance': 'EgYIBRABGAI%3D',
'any,any,age': 'CAISAhAB',
'hour,any,age': 'CAISBAgBEAE%3D',
'day,any,age': 'CAISBAgCEAE%3D',
'week,any,age': 'CAISBAgDEAE%3D',
'month,any,age': 'CAISBAgEEAE%3D',
'year,any,age': 'CAISBAgFEAE%3D',
'any,short,age': 'CAISBBABGAE%3D',
'hour,short,age': 'CAISBggBEAEYAQ%3D%3D',
'day,short,age': 'CAISBggCEAEYAQ%3D%3D',
'week,short,age': 'CAISBggDEAEYAQ%3D%3D',
'month,short,age': 'CAISBggEEAEYAQ%3D%3D',
'year,short,age': 'CAISBggFEAEYAQ%3D%3D',
'any,long,age': 'CAISBBABGAI%3D',
'hour,long,age': 'CAISBggBEAEYAg%3D%3D',
'day,long,age': 'CAISBggCEAEYAg%3D%3D',
'week,long,age': 'CAISBggDEAEYAg%3D%3D',
'month,long,age': 'CAISBggEEAEYAg%3D%3D',
'year,long,age': 'CAISBggFEAEYAg%3D%3D',
'any,any,views': 'CAMSAhAB',
'hour,any,views': 'CAMSBAgBEAE%3D',
'day,any,views': 'CAMSBAgCEAE%3D',
'week,any,views': 'CAMSBAgDEAE%3D',
'month,any,views': 'CAMSBAgEEAE%3D',
'year,any,views': 'CAMSBAgFEAE%3D',
'any,short,views': 'CAMSBBABGAE%3D',
'hour,short,views': 'CAMSBggBEAEYAQ%3D%3D',
'day,short,views': 'CAMSBggCEAEYAQ%3D%3D',
'week,short,views': 'CAMSBggDEAEYAQ%3D%3D',
'month,short,views': 'CAMSBggEEAEYAQ%3D%3D',
'year,short,views': 'CAMSBggFEAEYAQ%3D%3D',
'any,long,views': 'CAMSBBABGAI%3D',
'hour,long,views': 'CAMSBggBEAEYAg%3D%3D',
'day,long,views': 'CAMSBggCEAEYAg%3D%3D',
'week,long,views': 'CAMSBggDEAEYAg%3D%3D',
'month,long,views': 'CAMSBggEEAEYAg%3D%3D',
'year,long,views': 'CAMSBggFEAEYAg%3D%3D',
'any,any,rating': 'CAESAhAB',
'hour,any,rating': 'CAESBAgBEAE%3D',
'day,any,rating': 'CAESBAgCEAE%3D',
'week,any,rating': 'CAESBAgDEAE%3D',
'month,any,rating': 'CAESBAgEEAE%3D',
'year,any,rating': 'CAESBAgFEAE%3D',
'any,short,rating': 'CAESBBABGAE%3D',
'hour,short,rating': 'CAESBggBEAEYAQ%3D%3D',
'day,short,rating': 'CAESBggCEAEYAQ%3D%3D',
'week,short,rating': 'CAESBggDEAEYAQ%3D%3D',
'month,short,rating': 'CAESBggEEAEYAQ%3D%3D',
'year,short,rating': 'CAESBggFEAEYAQ%3D%3D',
'any,long,rating': 'CAESBBABGAI%3D',
'hour,long,rating': 'CAESBggBEAEYAg%3D%3D',
'day,long,rating': 'CAESBggCEAEYAg%3D%3D',
'week,long,rating': 'CAESBggDEAEYAg%3D%3D',
'month,long,rating': 'CAESBggEEAEYAg%3D%3D',
'year,long,rating': 'CAESBggFEAEYAg%3D%3D'
})[order] || 'EgIQAQ%3D%3D');
}
return headers;
};
const formatVideoData = (data, context, desktop) => {
let video_details = {};
let metadata = {};
if (desktop) {
metadata.embed = data.microformat.playerMicroformatRenderer.embed;
metadata.view_count = parseInt(data.videoDetails.viewCount);
metadata.average_rating = data.videoDetails.averageRating;
metadata.length_seconds = data.microformat.playerMicroformatRenderer.lengthSeconds;
metadata.channel_id = data.videoDetails.channelId;
metadata.channel_url = data.microformat.playerMicroformatRenderer.ownerProfileUrl;
metadata.external_channel_id = data.microformat.playerMicroformatRenderer.externalChannelId;
metadata.is_live_content = data.videoDetails.isLiveContent;
metadata.is_family_safe = data.microformat.playerMicroformatRenderer.isFamilySafe;
metadata.is_unlisted = data.microformat.playerMicroformatRenderer.isUnlisted;
metadata.is_private = data.videoDetails.isPrivate;
metadata.has_ypc_metadata = data.microformat.playerMicroformatRenderer.hasYpcMetadata;
metadata.category = data.microformat.playerMicroformatRenderer.category;
metadata.channel_name = data.microformat.playerMicroformatRenderer.ownerChannelName;
metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate || 'N/A';
metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate || 'N/A';
metadata.keywords = data.videoDetails.keywords || [];
video_details.title = data.videoDetails.title;
video_details.description = data.videoDetails.shortDescription;
video_details.thumbnail = data.videoDetails.thumbnail.thumbnails.slice(-1)[0];
video_details.metadata = metadata;
} else {
metadata.embed = data[2].playerResponse.microformat.playerMicroformatRenderer.embed;
metadata.likes = parseInt(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, ''));
metadata.dislikes = parseInt(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, ''));
metadata.view_count = parseInt(data[2].playerResponse.videoDetails.viewCount);
metadata.average_rating = data[2].playerResponse.videoDetails.averageRating;
metadata.length_seconds = data[2].playerResponse.microformat.playerMicroformatRenderer.lengthSeconds;
metadata.channel_id = data[2].playerResponse.videoDetails.channelId;
metadata.channel_url = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerProfileUrl;
metadata.external_channel_id = data[2].playerResponse.microformat.playerMicroformatRenderer.externalChannelId;
metadata.is_live_content = data[2].playerResponse.videoDetails.isLiveContent;
metadata.is_family_safe = data[2].playerResponse.microformat.playerMicroformatRenderer.isFamilySafe;
metadata.is_unlisted = data[2].playerResponse.microformat.playerMicroformatRenderer.isUnlisted;
metadata.is_private = data[2].playerResponse.videoDetails.isPrivate;
metadata.has_ypc_metadata = data[2].playerResponse.microformat.playerMicroformatRenderer.hasYpcMetadata;
metadata.category = data[2].playerResponse.microformat.playerMicroformatRenderer.category;
metadata.channel_name = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerChannelName;
metadata.publish_date = data[2].playerResponse.microformat.playerMicroformatRenderer.publishDate;
metadata.upload_date = data[2].playerResponse.microformat.playerMicroformatRenderer.uploadDate;
metadata.keywords = data[2].playerResponse.videoDetails.keywords;
video_details.title = data[2].playerResponse.videoDetails.title;
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
// actions
video_details.like = like => {};
video_details.dislike = dislike => {};
video_details.removeLike = remove_like => {};
video_details.subscribe = subscribe => {};
video_details.unsubscribe = unsubscribe => {};
video_details.comment = comment => {};
// additional metadata
video_details.metadata = metadata;
}
return video_details;
};
const filters = (order) => {
switch (order) {
case 'any,any,relevance':
return 'EgIQAQ%3D%3D';
case 'hour,any,relevance':
return 'EgIIAQ%3D%3D';
case 'day,any,relevance':
return 'EgQIAhAB';
case 'week,any,relevance':
return 'EgQIAxAB';
case 'month,any,relevance':
return 'EgQIBBAB';
case 'year,any,relevance':
return 'EgQIBRAB';
case 'any,short,relevance':
return 'EgQQARgB';
case 'hour,short,relevance':
return 'EgYIARABGAE%3D';
case 'day,short,relevance':
return 'EgYIAhABGAE%3D';
case 'week,short,relevance':
return 'EgYIAxABGAE%3D';
case 'month,short,relevance':
return 'EgYIBBABGAE%3D';
case 'year,short,relevance':
return 'EgYIBRABGAE%3D';
case 'any,long,relevance':
return 'EgQQARgC';
case 'hour,long,relevance':
return 'EgYIARABGAI%3D';
case 'day,long,relevance':
return 'EgYIAhABGAI%3D';
case 'week,long,relevance':
return 'EgYIAxABGAI%3D';
case 'month,long,relevance':
return 'EgYIBBABGAI%3D';
case 'year,long,relevance':
return 'EgYIBRABGAI%3D';
case 'any,any,age':
return 'CAISAhAB';
case 'hour,any,age':
return 'CAISBAgBEAE%3D';
case 'day,any,age':
return 'CAISBAgCEAE%3D';
case 'week,any,age':
return 'CAISBAgDEAE%3D';
case 'month,any,age':
return 'CAISBAgEEAE%3D';
case 'year,any,age':
return 'CAISBAgFEAE%3D';
case 'any,short,age':
return 'CAISBBABGAE%3D';
case 'hour,short,age':
return 'CAISBggBEAEYAQ%3D%3D';
case 'day,short,age':
return 'CAISBggCEAEYAQ%3D%3D';
case 'week,short,age':
return 'CAISBggDEAEYAQ%3D%3D';
case 'month,short,age':
return 'CAISBggEEAEYAQ%3D%3D';
case 'year,short,age':
return 'CAISBggFEAEYAQ%3D%3D';
case 'any,long,age':
return 'CAISBBABGAI%3D';
case 'hour,long,age':
return 'CAISBggBEAEYAg%3D%3D';
case 'day,long,age':
return 'CAISBggCEAEYAg%3D%3D';
case 'week,long,age':
return 'CAISBggDEAEYAg%3D%3D';
case 'month,long,age':
return 'CAISBggEEAEYAg%3D%3D';
case 'year,long,age':
return 'CAISBggFEAEYAg%3D%3D';
case 'any,any,views':
return 'CAMSAhAB';
case 'hour,any,views':
return 'CAMSBAgBEAE%3D';
case 'day,any,views':
return 'CAMSBAgCEAE%3D';
case 'week,any,views':
return 'CAMSBAgDEAE%3D';
case 'month,any,views':
return 'CAMSBAgEEAE%3D';
case 'year,any,views':
return 'CAMSBAgFEAE%3D';
case 'any,short,views':
return 'CAMSBBABGAE%3D';
case 'hour,short,views':
return 'CAMSBggBEAEYAQ%3D%3D';
case 'day,short,views':
return 'CAMSBggCEAEYAQ%3D%3D';
case 'week,short,views':
return 'CAMSBggDEAEYAQ%3D%3D';
case 'month,short,views':
return 'CAMSBggEEAEYAQ%3D%3D';
case 'year,short,views':
return 'CAMSBggFEAEYAQ%3D%3D';
case 'any,long,views':
return 'CAMSBBABGAI%3D';
case 'hour,long,views':
return 'CAMSBggBEAEYAg%3D%3D';
case 'day,long,views':
return 'CAMSBggCEAEYAg%3D%3D';
case 'week,long,views':
return 'CAMSBggDEAEYAg%3D%3D';
case 'month,long,views':
return 'CAMSBggEEAEYAg%3D%3D';
case 'year,long,views':
return 'CAMSBggFEAEYAg%3D%3D';
case 'any,any,rating':
return 'CAESAhAB';
case 'hour,any,rating':
return 'CAESBAgBEAE%3D';
case 'day,any,rating':
return 'CAESBAgCEAE%3D';
case 'week,any,rating':
return 'CAESBAgDEAE%3D';
case 'month,any,rating':
return 'CAESBAgEEAE%3D';
case 'year,any,rating':
return 'CAESBAgFEAE%3D';
case 'any,short,rating':
return 'CAESBBABGAE%3D';
case 'hour,short,rating':
return 'CAESBggBEAEYAQ%3D%3D';
case 'day,short,rating':
return 'CAESBggCEAEYAQ%3D%3D';
case 'week,short,rating':
return 'CAESBggDEAEYAQ%3D%3D';
case 'month,short,rating':
return 'CAESBggEEAEYAQ%3D%3D';
case 'year,short,rating':
return 'CAESBggFEAEYAQ%3D%3D';
case 'any,long,rating':
return 'CAESBBABGAI%3D';
case 'hour,long,rating':
return 'CAESBggBEAEYAg%3D%3D';
case 'day,long,rating':
return 'CAESBggCEAEYAg%3D%3D';
case 'week,long,rating':
return 'CAESBggDEAEYAg%3D%3D';
case 'month,long,rating':
return 'CAESBggEEAEYAg%3D%3D';
case 'year,long,rating':
return 'CAESBggFEAEYAg%3D%3D';
default:
}
};
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, filters };
};

View File

@@ -5,46 +5,56 @@ 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');
const SigDecipher = require('./SigDecipher');
const SigDecipher = require('./Sig');
const EventEmitter = require('events');
const TimeToSeconds = require('time-to-seconds');
const CancelToken = Axios.CancelToken;
class Innertube extends EventEmitter {
constructor(cookie, sign_in) {
constructor(cookie) {
super();
this.cookie = cookie || '';
this.retry_count = 0;
return this.init();
}
async init() {
const response = await Axios.get(Constants.urls.YT_BASE_URL, Constants.default_headers(this)).catch((error) => error);
if (response instanceof Error) throw new Error('Could not retrieve Innertube configuration data: ' + response.message);
let innertube_data = JSON.parse('{' + Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') + '}');
if (innertube_data.INNERTUBE_CONTEXT) {
this.context = innertube_data.INNERTUBE_CONTEXT;
this.key = innertube_data.INNERTUBE_API_KEY;
this.id_token = innertube_data.ID_TOKEN;
this.session_token = innertube_data.XSRF_TOKEN;
this.player_url = innertube_data.PLAYER_JS_URL;
this.logged_in = innertube_data.LOGGED_IN;
this.sts = innertube_data.STS;
this.context.client.hl = 'en';
this.context.client.gl = 'US';
const response = await Axios.get(Constants.URLS.YT_BASE_URL, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not extract Innertube data: ${response.message}`);
this.player = new Player(this);
await this.player.init();
try {
const innertube_data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`);
if (innertube_data.INNERTUBE_CONTEXT) {
this.context = innertube_data.INNERTUBE_CONTEXT;
this.key = innertube_data.INNERTUBE_API_KEY;
this.id_token = innertube_data.ID_TOKEN;
this.session_token = innertube_data.XSRF_TOKEN;
this.player_url = innertube_data.PLAYER_JS_URL;
this.logged_in = innertube_data.LOGGED_IN;
this.sts = innertube_data.STS;
this.context.client.hl = 'en';
this.context.client.gl = 'US';
if (this.logged_in && this.cookie.length > 1) {
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
this.player = new Player(this);
await this.player.init();
if (this.logged_in && this.cookie.length > 1) {
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
} else {
this.retry_count += 1;
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
return this.init();
}
this.initialized = true;
} else {
this.initialized = false;
} catch (err) {
this.retry_count += 1;
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
return this.init();
}
return this;
}
@@ -90,43 +100,43 @@ class Innertube extends EventEmitter {
}
async search(query, options = { period: 'any', order: 'relevance', duration: 'any' }) {
if (!this.initialized) throw new Error('Missing Innertube data.');
if (!query) throw new Error('No query was provided');
const yt_response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/search${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.innertube_request_opts({ session: this })).catch((error) => error);
if (yt_response instanceof Error) throw new Error('Could not search on YouTube: ' + yt_response.message);
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/search${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.INNERTUBE_REQOPTS({ session: this })).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not search on YouTube: ${response.message}`);
let content = yt_response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
let content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
let search_response = {};
search_response.search_metadata = {};
search_response.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query;
search_response.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query;
search_response.search_metadata.estimated_results = parseInt(yt_response.data.estimatedResults);
search_response.search_metadata.estimated_results = parseInt(response.data.estimatedResults);
search_response.videos = content.map((data) => {
if (!data.videoRenderer) return;
let video = data.videoRenderer;
return {
title: video.title.runs[0].text,
description: video.detailedMetadataSnippets ? video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') : 'N/A',
description: video.detailedMetadataSnippets && video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') || 'N/A',
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,
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',
view_count: video.viewCountText && video.viewCountText.simpleText || 'N/A',
short_view_count_text: {
simple_text: video.shortViewCountText ? video.shortViewCountText.simpleText : 'N/A',
accessibility_label: video.shortViewCountText ? (video.shortViewCountText.accessibility ? video.shortViewCountText.accessibility.accessibilityData.label : 'N/A') : 'N/A',
simple_text: video.shortViewCountText && video.shortViewCountText.simpleText || 'N/A',
accessibility_label: video.shortViewCountText && (video.shortViewCountText.accessibility && video.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
},
thumbnails: video.thumbnail.thumbnails,
duration: {
seconds: TimeToSeconds(video.lengthText ? video.lengthText.simpleText : '0'),
simple_text: video.lengthText ? video.lengthText.simpleText : 'N/A',
accessibility_label: video.lengthText ? video.lengthText.accessibility.accessibilityData.label : 'N/A'
seconds: TimeToSeconds(video.lengthText && video.lengthText.simpleText || '0'),
simple_text: video.lengthText && video.lengthText.simpleText || 'N/A',
accessibility_label: video.lengthText && video.lengthText.accessibility.accessibilityData.label || 'N/A'
},
published: video.publishedTimeText ? video.publishedTimeText.simpleText : 'N/A',
badges: video.badges ? video.badges.map((item) => item.metadataBadgeRenderer.label) : 'N/A',
owner_badges: video.ownerBadges ? video.ownerBadges.map((item) => item.metadataBadgeRenderer.tooltip) : 'N/A '
published: video.publishedTimeText && video.publishedTimeText.simpleText || 'N/A',
badges: video.badges && video.badges.map((item) => item.metadataBadgeRenderer.label) || 'N/A',
owner_badges: video.ownerBadges && video.ownerBadges.map((item) => item.metadataBadgeRenderer.tooltip) || 'N/A '
}
};
}).filter((video_block) => video_block !== undefined);
@@ -134,23 +144,124 @@ class Innertube extends EventEmitter {
}
async getDetails(id) {
if (!id) return { error: 'Missing video id' };
const data = await this.requestVideoInfo(id, false);
const video_data = Constants.formatVideoData(data, this, false);
video_data.like = like => Actions.likeVideo(this, id);
video_data.dislike = dislike => Actions.dislikeVideo(this, id);
video_data.removeLike = remove_like => Actions.removeLike(this, id);
video_data.subscribe = subscribe => Actions.subscribe(this, id, video_data.metadata.channel_id);
video_data.unsubscribe = unsubscribe => Actions.unsubscribe(this, id, video_data.metadata.channel_id);
video_data.comment = comment => Actions.commentVideo(this, id, comment);
if (video_data.metadata.is_live_content) {
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 });
video_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: id });
video_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id: id });
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 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');
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 && thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText || 'N/A',
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.getNotifications(this);
const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuNotificationSectionRenderer.items;
return contents.map((notification) => {
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) => {
if (!notification.notificationRenderer) return;
notification = notification.notificationRenderer;
return {
@@ -159,25 +270,30 @@ class Innertube extends EventEmitter {
channel_name: notification.contextualMenu.menuRenderer.items[1].menuServiceItemRenderer.text.runs[1].text,
channel_thumbnail: notification.thumbnail.thumbnails[0],
video_thumbnail: notification.videoThumbnail.thumbnails[0],
video_url: 'https://youtu.be/' + notification.navigationEndpoint.watchEndpoint.videoId,
video_url: `https://youtu.be/${notification.navigationEndpoint.watchEndpoint.videoId}`,
read: notification.read,
notification_id: notification.notificationId,
};
}).filter((notification_block) => notification_block);
}
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;
}
async requestVideoInfo(id, desktop) {
let response;
if (!desktop) {
response = await Axios.get(Constants.urls.YT_WATCH_PAGE + '?v=' + id + 't=8s&pbj=1&bpctr=9999999999&has_verified=1&', Constants.innertube_request_opts({ session: this, id, desktop: false })).catch((error) => error);
} else {
response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/player${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify(Constants.video_details_reqbody(id, this.sts, this.context)), Constants.innertube_request_opts({ session: this, id, desktop: true })).catch((error) => error);
}
!desktop && (response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: false })).catch((error) => error)) ||
(response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(id, this.sts, this.context)), Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: true })).catch((error) => error));
if (response instanceof Error) throw new Error('Could not retrieve watch page info: ' + response.message);
return response.data;
}
download(id, options = {}) {
if (!id) throw new Error('Missing video id');
options.quality = options.quality || '360p';
options.type = options.type || 'videoandaudio';
options.format = options.format || 'mp4';
@@ -197,12 +313,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();
}
@@ -240,15 +356,10 @@ class Innertube extends EventEmitter {
if (options.type != 'videoandaudio') {
let streams;
if (options.type != 'audio') {
streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality);
} else {
streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4'));
}
options.type != 'audio' && (streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
(streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4')));
if (streams == undefined || streams.length == 0) {
streams = filtered_streams.filter((format) => format.quality == 'medium');
}
streams == undefined || streams.length == 0 && (streams = filtered_streams.filter((format) => format.quality == 'medium'));
bitrates = streams.map((format) => format.bitrate);
url = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];
@@ -256,13 +367,18 @@ class Innertube extends EventEmitter {
const selected_format = options.type == 'videoandaudio' ? filtered_streams[0] : url;
if (!selected_format) {
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMATS_UNAVAILABLE' });
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMAT_UNAVAILABLE' });
} else {
stream.emit('info', { video_details, selected_format, formats });
}
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);
if (options.type == 'videoandaudio' && !options.range) {
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;
@@ -286,18 +402,27 @@ class Innertube extends EventEmitter {
}
});
response.data.pipe(stream, true);
response.data.pipe(stream, { end: true });
} else {
const chunk_size = 1048576 * 10; // 10MB
let chunk_start = 0;
let chunk_end = chunk_size;
let chunk_start = (options.range && options.range.start || 0);
let chunk_end = (options.range && options.range.end || chunk_size);
let downloaded_size = 0;
let must_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);
(chunk_end >= selected_format.contentLength || options.range) && (must_end = true);
options.range && (selected_format.contentLength = options.range.end);
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;
@@ -310,17 +435,6 @@ class Innertube extends EventEmitter {
stream.emit('progress', { chunk_size: chunk.length, downloaded_size: (downloaded_size / 1024 / 1024).toFixed(2), percentage, size, raw_data: { chunk_size: chunk.length, downloaded: downloaded_size, size: response.headers['content-length'] } });
});
response.data.on('end', () => {
chunk_start = chunk_end + 1;
chunk_end += chunk_size;
if (downloaded_size < selected_format.contentLength) {
downloadChunk();
} else {
stream.emit('end');
}
});
response.data.on('error', (err) => {
if (cancelled) {
stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' });
@@ -329,7 +443,15 @@ class Innertube extends EventEmitter {
}
});
response.data.pipe(stream, { end: false });
response.data.on('end', () => {
if (!must_end && !options.range) {
chunk_start = chunk_end + 1;
chunk_end += chunk_size;
downloadChunk();
}
});
response.data.pipe(stream, { end: must_end });
};
downloadChunk();
}
@@ -339,6 +461,7 @@ class Innertube extends EventEmitter {
cancelled = true;
cancel();
};
return stream;
}
}

132
lib/Livechat.js Normal file
View File

@@ -0,0 +1,132 @@
'use strict';
const Axios = require('axios');
const Actions = require('./Actions');
const Constants = require('./Constants');
const EventEmitter = require('events');
class Livechat extends EventEmitter {
constructor(session, token, channel_id, video_id) {
super(session);
this.ctoken = token;
this.session = session;
this.video_id = video_id;
this.channel_id = channel_id;
this.message_queue = [];
this.id_cache = [];
this.poll_intervals_ms = 1000;
this.running = true;
this.poll();
}
enqueueActionGroup(group) {
group.forEach((action) => {
if (!action.addChatItemAction) return; //TODO: handle different action types
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer;
if (!message_content) return;
const message = {
text: message_content.message.runs.map((item) => item.text).join(' '),
author: {
name: message_content.authorName && message_content.authorName.simpleText || 'N/',
channel_id: message_content.authorExternalChannelId,
profile_picture: message_content.authorPhoto.thumbnails
},
timestamp: message_content.timestampUsec,
id: message_content.id
};
this.message_queue.push(message);
});
}
async poll() {
if (!this.running) return;
let data;
data = { context: this.session.context, continuation: this.ctoken };
const livechat = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/live_chat/get_live_chat${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true }));
if (livechat instanceof Error) throw new Error(`Error polling livechat: ${livechat.message}`);
const continuation_contents = livechat.data.continuationContents;
const action_group = continuation_contents.liveChatContinuation.actions;
this.enqueueActionGroup(action_group);
// Why don't we just emit the message directly? Well, enqueueing the messages is necessary so they are not emitted in a “messy” way, funny enough that's exactly how YouTube does it in its live chat js script.
this.message_queue.forEach((message, index) => {
if (this.id_cache.includes(message.id)) return;
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;
const updated_metadata = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/updated_metadata${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true }));
if (updated_metadata instanceof Error) throw new Error(`Error polling updated metadata: ${updated_metadata.message}`);
this.metadata_ctoken = updated_metadata.data.continuation.timedContinuationData.continuation;
const metadata = updated_metadata.data.actions;
this.emit('update-metadata', {
likes: metadata[1].updateToggleButtonTextAction.defaultText.simpleText,
dislikes: metadata[2].updateToggleButtonTextAction.defaultText.simpleText,
view_count: {
simple_text: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.viewCount.simpleText,
short_view_count: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.extraShortViewCount.simpleText
}
});
this.livechat_poller = setTimeout(async () => await this.poll(), this.poll_intervals_ms);
}
async sendMessage(text) {
const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
if (!message.success) return message;
const deleteMessage = async () => {
const menu = await Actions.livechat(this.session, 'live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
if (!menu.success) return menu;
const chat_item_menu = menu.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
if (!cmd.success) return cmd;
return { success: true, status_code: cmd.status_code };
};
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() {
this.running = false;
clearTimeout(this.livechat_poller);
}
}
module.exports = Livechat;

111
lib/NToken.js Normal file
View File

@@ -0,0 +1,111 @@
'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_calls_regex = /c\[(.*?)\]\((.+?)\)/g;
}
transform(n) {
let n_token = n.split('');
try {
let transformations = this.getTransformationData(this.raw_code);
transformations = transformations.map((el) => {
if (el != null && typeof el != 'number') {
const is_reverse_base64 = el.includes('case 65:');
(({ // Identifies the transformation functions and emulates them accordingly.
[Constants.FUNCS.PUSH]: () => el = (arr, i) => this.push(arr, i),
[Constants.FUNCS.SPLICE]: () => el = (arr, i) => this.splice(arr, i),
[Constants.FUNCS.SWAP0_1]: () => el = (arr, i) => this.swap0(arr, i),
[Constants.FUNCS.SWAP0_2]: () => el = (arr, i) => this.swap0(arr, i),
[Constants.FUNCS.ROTATE_1]: () => el = (arr, i) => this.rotate(arr, i),
[Constants.FUNCS.ROTATE_2]: () => el = (arr, i) => this.rotate(arr, i),
[Constants.FUNCS.REVERSE_1]: () => el = (arr) => this.reverse(arr),
[Constants.FUNCS.REVERSE_2]: () => el = (arr) => this.reverse(arr),
[Constants.FUNCS.BASE64_DIA]: () => el = () => this.getBase64Dia(is_reverse_base64),
[Constants.FUNCS.TRANSLATE_1]: () => el = (arr, token) => this.translate1(arr, token, is_reverse_base64),
[Constants.FUNCS.TRANSLATE_2]: () => el = (arr, token, base64_dic) => this.translate2(arr, token, base64_dic)
})[this.getFunc(el)] || (() => 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_calls = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_calls_regex)].map((params) => ({ index: params[1], params: params[2] }));
transformation_calls.forEach((data) => {
const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]);
const base64_dia = (param_index[2] && transformations[param_index[2]]());
transformations[data.index](transformations[param_index[0]], transformations[param_index[1]], base64_dia);
});
} catch (err) {
console.error('Could not transform n-token, download may be throttled:', err);
return n;
}
return n_token.join('');
}
getFunc(el) {
return el.match(Constants.FUNCS_REGEX);
}
getTransformationData() {
const data = `[${Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'c=[', '];c')}]`;
return JSON.parse(Constants.formatNTransformData(data));
}
translate1(arr, token, is_reverse_base64) {
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL;
arr.forEach(function(char, index, loc) {
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]);
}, token.split(''));
}
translate2(arr, token, characters) {
let chars_length = characters.length;
arr.forEach(function(char, index, loc) {
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + index + chars_length--) % characters.length]);
}, token.split(''));
}
getBase64Dia(is_reverse_base64) {
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL;
return characters;
}
swap0(arr, index) {
const old_value = arr[0];
index = (index % arr.length + arr.length) % arr.length;
arr[0] = arr[index];
arr[index] = old_value;
}
rotate(arr, index) {
index = (index % arr.length + arr.length) % arr.length;
arr.splice(-index).reverse().forEach((el) => arr.unshift(el));
}
splice(arr, index) {
index = (index % arr.length + arr.length) % arr.length;
arr.splice(index, 1);
}
reverse(arr) {
arr.reverse();
}
push(arr, item) {
arr.push(item);
}
}
module.exports = NToken;

View File

@@ -7,49 +7,97 @@ const EventEmitter = require('events');
const Uuid = require("uuid");
class OAuth extends EventEmitter {
constructor (creds) {
constructor(creds) {
super();
// Default interval between requests when waiting for authorization.
this.refresh_interval = 5;
// OAuth URLs:
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.oauth_code_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/device/code`;
this.oauth_token_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/token`;
// Used to check whether an access token is valid or not.
this.guide_url = `${Constants.urls.YT_BASE_URL}/youtubei/v1/guide`;
this.guide_url = `${Constants.URLS.YT_BASE_URL}/youtubei/v1/guide`;
// These are always the same, so we shouldn't have any problems for now.
this.model_name = Constants.oauth.model_name;
this.grant_type = Constants.oauth.grant_type;
this.scope = Constants.oauth.scope;
this.model_name = Constants.OAUTH.MODEL_NAME;
this.grant_type = Constants.OAUTH.GRANT_TYPE;
this.scope = Constants.OAUTH.SCOPE;
// Script that contains important information such as client id and client secret.
this.auth_script_regex = /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/;
// Used to find the credentials inside the script.
this.identity_regex = /var .+?=\"(?<id>.+?)\",[.|\s].?=\"(?<secret>.+?)\"/;
this.identity_regex = /var .+?=\"(?<id>.+?)\",.+?=\"(?<secret>.+?)\"/;
if (creds.access_token != undefined && creds.refresh_token != undefined) return;
this.requestAuthCode();
}
async waitForAuth(device_code) {
async requestAuthCode() {
const identity = await this.getClientIdentity();
this.client_id = identity.id;
this.client_secret = identity.secret;
const data = {
client_id: this.client_id,
scope: this.scope,
device_id: Uuid.v4(),
model_name: this.model_name
};
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
if (response instanceof Error)
return this.emit('auth', {
error: 'Could not get auth code.',
status: 'FAILED'
});
this.emit('auth', {
code: response.data.user_code,
status: 'AUTHORIZATION_PENDING',
expires_in: response.data.expires_in,
verification_url: response.data.verification_url
});
this.refresh_interval = response.data.interval;
// Keeps requesting at a specific rate until the authorization is granted or denied.
this.waitForAuth(response.data.device_code);
}
async getClientIdentity() {
// The first 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);
if (yttv_response instanceof Error) throw new Error(`Could not extract client identify: ${yttv_response.message}`);
// Here we get 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 response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not extract client identify: ${response.message}`);
const identity_function = Utils.getStringBetweenStrings(response.data, 'setQuery("");', '{useGaiaSandbox:');
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex);
return client_identity.groups;
}
waitForAuth(device_code) {
const data = {
client_id: this.client_id,
client_secret: this.client_secret,
code: device_code,
grant_type: this.grant_type
};
setTimeout(async () => {
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
if (response instanceof Error)
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
if (response instanceof Error)
return this.emit('auth', {
error: 'Could not get auth token.',
status: 'FAILED'
});
if (response.data.error) {
switch (response.data.error) {
case 'slow_down':
@@ -73,7 +121,7 @@ class OAuth extends EventEmitter {
}
} else {
this.emit('auth', {
access_token: response.data.access_token,
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
token_type: response.data.token_type,
expires: response.data.expires_in,
@@ -83,86 +131,38 @@ class OAuth extends EventEmitter {
}
}, 1000 * this.refresh_interval);
}
async requestAuthCode() {
async refreshAccessToken(refresh_token) {
const identity = await this.getClientIdentity();
this.client_id = identity.id;
this.client_secret = identity.secret;
const data = {
client_id: this.client_id,
scope: this.scope,
device_id : Uuid.v4(),
model_name: this.model_name
};
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
if (response instanceof Error)
return this.emit('auth', {
error: 'Could not get auth code.',
status: 'FAILED'
});
this.emit('auth', {
code: response.data.user_code,
status: 'AUTHORIZATION_PENDING',
expires_in: response.data.expires_in,
verification_url: response.data.verification_url
});
this.refresh_interval = response.data.interval;
// Keeps requesting at a specific rate until the authorization is granted or denied.
this.waitForAuth(response.data.device_code);
}
async getClientIdentity() {
// The first 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_reqopts).catch((error) => error);
if (yttv_response instanceof Error) throw new Error(`Could not get identify: ${yttv_response.message}`);
// Here we get 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 response = await Axios.get(script_url, Constants.default_headers).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not fetch data from auth script: ${response.message}`);
const identify_function = Utils.getStringBetweenStrings(response.data, '=function(){var a=window.environment', '(function()');
const client_identity = identify_function.match(this.identity_regex).groups;
return client_identity;
}
async refreshAccessToken (refresh_token) {
const identity = await this.getClientIdentity();
const data = {
client_id: identity.id,
client_secret: identity.secret,
refresh_token,
grant_type : 'refresh_token',
grant_type: 'refresh_token',
};
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
if (response instanceof Error)
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
if (response instanceof Error)
return this.emit('refresh-token', {
error: 'Could not refresh token.',
error: 'Could not refresh access token.',
status: 'FAILED'
});
this.emit('refresh-token', {
access_token: response.data.access_token,
access_token: response.data.access_token,
token_type: response.data.token_type,
expires: response.data.expires_in,
scope: response.data.scope,
status: 'SUCCESS'
});
}
async checkTokenValidity(access_token, session) {
let headers = Constants.innertube_request_opts({ session }).headers;
let headers = Constants.INNERTUBE_REQOPTS({ session }).headers;
headers.authorization = `Bearer ${access_token}`;
const response = await Axios.post(this.guide_url, JSON.stringify({ context : session.context }), { headers }).catch((error) => error);
const response = await Axios.post(this.guide_url, JSON.stringify({ context: session.context }), { headers }).catch((error) => error);
if (response instanceof Error) return 'INVALID';
return 'VALID';
}

View File

@@ -13,16 +13,19 @@ class Player {
}
async init() {
if (fs.existsSync(this.tmp_cache_dir + '/' + this.player_name + '.js')) {
const player_data = fs.readFileSync(this.tmp_cache_dir + '/' + this.player_name + '.js').toString();
if (fs.existsSync(`${this.tmp_cache_dir}/${this.player_name}.js`)) {
const player_data = fs.readFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`).toString();
this.getSigDecipherCode(player_data);
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);
if (response instanceof Error) throw new Error('Could not get player data: ' + response.message);
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);
if (response instanceof Error) throw new Error('Could not download player script: ' + response.message);
fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
fs.writeFileSync(this.tmp_cache_dir + '/' + this.player_name + '.js', response.data);
try {
// Caches the current player so we don't have to download it all the time
fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data);
} catch (err) {}
this.getSigDecipherCode(response.data);
this.getNEncoder(response.data);
@@ -30,14 +33,13 @@ class Player {
}
getSigDecipherCode(data) {
const actions_algorithm_code = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};');
const actions_sequence_code = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
this.sig_decipher_sc = actions_algorithm_code + actions_sequence_code;
const manipulation_algorithm_code = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};');
const manipulation_sequence_code = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
this.sig_decipher_sc = manipulation_algorithm_code + manipulation_sequence_code;
}
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("");`;
}
}

View File

@@ -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;
}
@@ -20,10 +20,10 @@ class SigDecipher {
arr.splice(0, end);
}
function swap(arr, position) {
function swap(arr, index) {
let origArrI = arr[0];
arr[0] = arr[position % arr.length];
arr[position % arr.length] = origArrI;
arr[0] = arr[index % arr.length];
arr[index % arr.length] = origArrI;
}
function reverse(arr) {
@@ -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]);
@@ -52,7 +52,7 @@ class SigDecipher {
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 +60,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')) {

View File

@@ -1,5 +1,7 @@
'use strict';
const Fs = require('fs');
const Proto = require('protons');
const Crypto = require('crypto');
const UserAgent = require('user-agents');
@@ -35,12 +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 encodeId(id) {
return encodeURI(new Buffer.from(` ` + id + `*`).toString('base64').replace('==', '') + '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');
}
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeId };
function generateCommentParams(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'));
}
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref };

34
lib/proto/youtube.proto Normal file
View 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;
}

115
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{
"name": "youtubei.js",
"version": "1.0.5",
"version": "1.2.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "1.0.5",
"version": "1.2.6",
"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"
@@ -53,9 +54,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"funding": [
{
"type": "individual",
@@ -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.10",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.10.tgz",
"integrity": "sha512-BwWGvgqB/5J/cnWaOA0sXzJ+UGl+kyFAw3Sw1L6TN4oad34C9OpW+GCpYTYPDp4pUaXDC1EjvB3yv9Iodo1EhA=="
},
"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.801",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.801.tgz",
"integrity": "sha512-giB7GP2g71STtQaYbSDpd5T+XzbGr5ni+1NpEbeQnifnFiOIQeQonXOC2kDxGKvubzul6qQb/BwG9LlIQ1zxXA==",
"version": "1.0.840",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.840.tgz",
"integrity": "sha512-pdXizW5mvcHRHsJH50STUA+zywDvUKbxouQ2523FEmfYJCkLMVGavBSOL7u/VVmneVXM2EX20Ikru9lCnbNDeQ==",
"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": {
@@ -146,20 +189,57 @@
}
},
"follow-redirects": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"multiformats": {
"version": "9.4.10",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.10.tgz",
"integrity": "sha512-BwWGvgqB/5J/cnWaOA0sXzJ+UGl+kyFAw3Sw1L6TN4oad34C9OpW+GCpYTYPDp4pUaXDC1EjvB3yv9Iodo1EhA=="
},
"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.801",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.801.tgz",
"integrity": "sha512-giB7GP2g71STtQaYbSDpd5T+XzbGr5ni+1NpEbeQnifnFiOIQeQonXOC2kDxGKvubzul6qQb/BwG9LlIQ1zxXA==",
"version": "1.0.840",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.840.tgz",
"integrity": "sha512-pdXizW5mvcHRHsJH50STUA+zywDvUKbxouQ2523FEmfYJCkLMVGavBSOL7u/VVmneVXM2EX20Ikru9lCnbNDeQ==",
"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=="
}
}
}
}

View File

@@ -1,10 +1,10 @@
{
"name": "youtubei.js",
"version": "1.0.5",
"version": "1.2.6",
"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": "echo \"Error: no test specified\" && exit 1"
"test": "node test"
},
"author": "LuanRT",
"license": "MIT",
@@ -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"
@@ -27,17 +28,19 @@
"youtube-dl",
"innertube",
"innertubeapi",
"livechat",
"api",
"search",
"like",
"dislike",
"comment",
"downloader",
"automation",
"downloader",
"comments-section",
"youtube-downloader"
],
"bugs": {
"url": "https://github.com/LuanRT/YouTube.js/issues"
},
"homepage": "https://github.com/LuanRT/YouTube.js#readme"
}
}

25
test/constants.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
module.exports = {
test_url: 's=t%3DQ%3DAv2TLJ2sbQFV5msp4j7v71gS1rsXNd6QH2V1KpxGlaOD%3DIC46mVzTVTW_2zttE32HKH7XO1jkyfOJs58avqMLKdvRdgIQRw8JQ0qOA&sp=sig&url=https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback%3Fexpire%3D1635863482%26ei%3DWveAYdqsB6KPobIPjtWwYA%26ip%3D128.201.98.50%26id%3Do-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7%26itag%3D18%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DG3%26mm%3D31%252C29%26mn%3Dsn-hxtxgcg-8qjl%252Csn-gpv7dned%26ms%3Dau%252Crdu%26mv%3Dm%26mvi%3D1%26pl%3D24%26initcwndbps%3D397500%26vprv%3D1%26mime%3Dvideo%252Fmp4%26ns%3Dv9CYauI2ycUgrV6wOERCNxsG%26gir%3Dyes%26clen%3D7275579%26ratebypass%3Dyes%26dur%3D218.290%26lmt%3D1540416860737282%26mt%3D1635841731%26fvip%3D4%26fexp%3D24001373%252C24007246%26c%3DWEB%26txp%3D5531432%26n%3DD8yGa-DC5m2Dwv--%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cratebypass%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX',
expected_url: 'https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback?expire=1635863482&ei=WveAYdqsB6KPobIPjtWwYA&ip=128.201.98.50&id=o-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7&itag=18&source=youtube&requiressl=yes&mh=G3&mm=31%2C29&mn=sn-hxtxgcg-8qjl%2Csn-gpv7dned&ms=au%2Crdu&mv=m&mvi=1&pl=24&initcwndbps=397500&vprv=1&mime=video%2Fmp4&ns=v9CYauI2ycUgrV6wOERCNxsG&gir=yes&clen=7275579&ratebypass=yes&dur=218.290&lmt=1540416860737282&mt=1635841731&fvip=4&fexp=24001373%2C24007246&c=WEB&txp=5531432&n=omhIaB28Jepv6Q&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX&sig=AOq0QJ8wRQIgdRvdKLMqva85sJOfykj1OX7HKH23Ettz2_WTVTzVm64CIQDOalGxpK1V2Ht6dNXsr1Sg17v7j4psm5VFQbs2JLT2vA%3D%3D&cver=2.20211101.01.00',
original_ntoken: 'PqjqqJjdB9K821VIisj',
expected_ntoken: 'AxwyS-osUl1WhMUd1',
client_version: '2.20211101.01.00',
test_video_id: 'FT_nzxtgXEw',
test_video_id_1: 'YE7VzlLtp-4',
sig_decipher_sc: `fB={RP:function(a,b){a.splice(0,b)},
Td:function(a){a.reverse()},
kq:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c};fB.kq(a,35);fB.RP(a,2);fB.kq(a,46);fB.Td(a,6);`,
n_scramble_sc: `var b=a.split(""),c=[-470482026,-691770757,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},
b,258876269,-1426380890,318754300,-68090711,-2064438462,-1886316521,1913911047,1635047330,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},
-1815897225,1940621629,-714586149,-1723898467,null,778601498,2145333248,1245726977,1952270083,268207944,244274044,null,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},
null,-762271981,604636391,1087224318,-931565987,-338396815,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},
2126741474,function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},
-1874551858,-1238260579,498106911,1913911047,-1951114300,-504396507,b,344510945,905306344,b,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},
909033134,1027812119,1686673079,function(d,e){d.push(e)},
-1902376100,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)},
"push",function(d,e){for(var f=64,h=[];++f-h.length-32;)switch(f){case 58:f=96;continue;case 91:f=44;break;case 65:f=47;continue;case 46:f=153;case 123:f-=58;default:h.push(String.fromCharCode(f))}d.forEach(function(l,m,n){this.push(n[m]=h[(h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length])},e.split(""))}];
c[17]=c;c[24]=c;c[26]=c;try{c[45](c[17],c[38]),c[12](c[44],c[29]),c[45](c[26],c[0]),c[51](c[41],c[13]),c[12](c[41],c[27]),c[12](c[26],c[11]),c[39](c[17],c[49]),c[9](c[38],c[47]),c[26](c[40],c[0]),c[7](c[8],c[44]),c[14](c[54],c[0]),c[18](c[3],c[25]),c[7](c[33],c[36]),c[15](c[19],c[14]),c[7](c[19],c[9]),c[7](c[6],c[12]),c[41](c[33],c[35]),c[7](c[40],c[5]),c[50](c[42]),c[13](c[14],c[17]),c[6](c[35],c[51]),c[26](c[48],c[50]),c[26](c[35],c[0]),c[6](c[21],c[46]),c[15](c[21],c[42]),c[1](c[2],c[43]),c[15](c[2],
c[31]),c[1](c[21],c[25]),c[22](c[30],c[17]),c[15](c[44],c[46]),c[22](c[44],c[11]),c[22](c[23],c[38]),c[1](c[23],c[14]),c[35](c[23],c[44]),c[11](c[53],c[20]),c[9](c[51]),c[31](c[51],c[28]),c[18](c[51],c[35]),c[46](c[53],c[6]),c[52](c[51],c[49]),c[11](c[53],c[15])}catch(d){return"enhanced_except_75MBkOz-_w8_"+a} return b.join("");`
};

57
test/index.js Normal file
View File

@@ -0,0 +1,57 @@
'use strict';
const Fs = require('fs');
const Innertube = require('..');
const NToken = require('../lib/NToken');
const SigDecipher = require('../lib/Sig');
const Constants = require('./constants');
let failed_tests = 0;
async function performTests() {
const youtube = await new Innertube().catch((error) => error);
assert(youtube instanceof Error ? false : true, `should retrieve Innertube configuration data`);
if (!(youtube instanceof Error)) {
const search = await youtube.search('Carl Sagan - Documentary').catch((error) => error);
assert((search instanceof Error ? false : true) && search.videos.length >= 1, `should search videos`);
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
assert(details instanceof Error ? false : true, `should retrieve details for ${Constants.test_video_id}`);
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error);
assert(comments instanceof Error ? false : true, `should retrieve comments for ${Constants.test_video_id}`);
const video = await downloadVideo(Constants.test_video_id_1, youtube).catch((error) => error);
assert(video instanceof Error ? false : true, `should download video (${Constants.test_video_id_1})`);
}
const n_token = new NToken(Constants.n_scramble_sc).transform(Constants.original_ntoken);
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`);
const transformed_url = new SigDecipher(Constants.test_url, Constants.client_version, { sig_decipher_sc: Constants.sig_decipher_sc, ntoken_sc: Constants.n_scramble_sc }).decipher();
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`);
if (failed_tests > 0)
throw new Error('Some tests have failed');
}
function downloadVideo(id, youtube) {
return new Promise((resolve, reject) => {
let got_video_info = false;
const stream = youtube.download(id, { type: 'videoandaudio' });
stream.pipe(Fs.createWriteStream(`./${id}.mp4`));
stream.on('end', () => Fs.existsSync(`./${id}.mp4`) && got_video_info && resolve() || reject());
stream.on('info', () => got_video_info = true);
stream.on('error', () => reject());
});
}
function assert(outcome, description) {
const pass_fail = outcome ? 'pass' : 'fail';
!outcome && (failed_tests += 1);
console.info(pass_fail, ':', description);
return outcome;
};
performTests();