mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-15 18:42:11 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8023e74adb | ||
|
|
09ce31061d | ||
|
|
7072485782 | ||
|
|
4bd79e5903 | ||
|
|
028e723226 | ||
|
|
1a68875aad | ||
|
|
de7d52a62c | ||
|
|
b8a2fd01cc | ||
|
|
b9ea6e36c8 | ||
|
|
783e6d2435 | ||
|
|
0b11e441ff | ||
|
|
d7db0d2304 | ||
|
|
d674eef530 | ||
|
|
17aa4edb66 | ||
|
|
656c0fb7b8 | ||
|
|
d4dce16be0 | ||
|
|
80a9ece314 |
155
README.md
155
README.md
@@ -1,6 +1,8 @@
|
||||
# YouTube.js
|
||||
|
||||
[](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml)
|
||||
[](https://www.npmjs.com/package/youtubei.js)
|
||||
[](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!
|
||||
|
||||
@@ -8,15 +10,20 @@ An object-oriented wrapper around the Innertube API, which is what YouTube itsel
|
||||
|
||||
As of now, this is one of the most advanced & stable YouTube libraries out there, and it can:
|
||||
|
||||
- Search.
|
||||
- Get detailed info about videos.
|
||||
- Fetch notifications (sign-in required).
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment (sign-in required).
|
||||
- Search
|
||||
- Get detailed info about videos
|
||||
- Fetch live chat & live stats in real time
|
||||
- Fetch notifications
|
||||
- Change notifications preferences for a channel
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment
|
||||
- Easily sign into your account without having to use cookies!
|
||||
- Last but not least, you can also download videos!
|
||||
|
||||
Do note that you must be signed-in to perform actions that involve an account, like commenting, subscribing, sending messages to a live chat, etc.
|
||||
|
||||
#### Do 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 +37,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:
|
||||
|
||||
@@ -243,65 +252,55 @@ const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.comment('Haha, nice!');
|
||||
```
|
||||
|
||||
* Changing notification preferences:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.setNotificationPref('ALL'); // ALL | NONE | PERSONALIZED
|
||||
```
|
||||
|
||||
All of the interactions above will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
|
||||
### Signing-in:
|
||||
|
||||
### Fetching live chats:
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats in an easy way plus you can also send messages!
|
||||
```js
|
||||
const fs = require('fs');
|
||||
const Innertube = require('youtubei.js');
|
||||
const creds_path = './yt_oauth_creds.json';
|
||||
|
||||
async function start() {
|
||||
const creds = fs.existsSync(creds_path) && JSON.parse(fs.readFileSync(creds_path).toString()) || {};
|
||||
const youtube = await new Innertube();
|
||||
|
||||
const search = await youtube.search('Some random live');
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
|
||||
// Only triggered when signing-in.
|
||||
youtube.on('auth', (data) => {
|
||||
if (data.status === 'AUTHORIZATION_PENDING') {
|
||||
console.info(`Hello!\nOn your phone or computer, go to ${data.verification_url} and enter the code ${data.code}`);
|
||||
} else if (data.status === 'SUCCESS') {
|
||||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token }));
|
||||
console.info('Successfully signed-in, enjoy!');
|
||||
// This should only be called if you're sure it's a live 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');
|
||||
}
|
||||
});
|
||||
|
||||
// Triggered whenever the access token is refreshed.
|
||||
youtube.on('update-credentials', (data) => {
|
||||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token }));
|
||||
console.info('Credentials updated!', data);
|
||||
});
|
||||
|
||||
await youtube.signIn(creds);
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
start();
|
||||
```
|
||||
|
||||
Cookies:
|
||||
|
||||
Deleting a message:
|
||||
```js
|
||||
const Innertube = require('youtubei.js');
|
||||
|
||||
async function start() {
|
||||
const youtube = await new Innertube(COOKIE_HERE);
|
||||
//...
|
||||
}
|
||||
|
||||
start();
|
||||
const msg = await livechat.sendMessage('Nice live!');
|
||||
await msg.deleteMessage();
|
||||
```
|
||||
|
||||
### Downloading videos:
|
||||
---
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
@@ -349,6 +348,65 @@ Cancelling a download:
|
||||
stream.cancel();
|
||||
```
|
||||
|
||||
### Signing-in:
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
const Innertube = require('youtubei.js');
|
||||
const creds_path = './yt_oauth_creds.json';
|
||||
|
||||
async function start() {
|
||||
const creds = fs.existsSync(creds_path) && JSON.parse(fs.readFileSync(creds_path).toString()) || {};
|
||||
const youtube = await new Innertube();
|
||||
|
||||
// Only triggered when signing-in.
|
||||
youtube.on('auth', (data) => {
|
||||
if (data.status === 'AUTHORIZATION_PENDING') {
|
||||
console.info(`Hello!\nOn your phone or computer, go to ${data.verification_url} and enter the code ${data.code}`);
|
||||
} else if (data.status === 'SUCCESS') {
|
||||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token }));
|
||||
console.info('Successfully signed-in, enjoy!');
|
||||
}
|
||||
});
|
||||
|
||||
// Triggered whenever the access token is refreshed.
|
||||
youtube.on('update-credentials', (data) => {
|
||||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token }));
|
||||
console.info('Credentials updated!', data);
|
||||
});
|
||||
|
||||
await youtube.signIn(creds);
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
start();
|
||||
```
|
||||
|
||||
Cookies:
|
||||
|
||||
```js
|
||||
const Innertube = require('youtubei.js');
|
||||
|
||||
async function start() {
|
||||
const youtube = await new Innertube(COOKIE_HERE);
|
||||
//...
|
||||
}
|
||||
|
||||
start();
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -358,5 +416,6 @@ This project is not affiliated with, endorsed, or sponsored by YouTube or any of
|
||||
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/)
|
||||
@@ -1,9 +1,10 @@
|
||||
'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');
|
||||
@@ -11,7 +12,8 @@ async function start() {
|
||||
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
console.info('Video details:', video);
|
||||
|
||||
if (video.error) return;
|
||||
|
||||
if (youtube.logged_in) {
|
||||
const myNotifications = await youtube.getNotifications();
|
||||
console.info('My notifications:', myNotifications);
|
||||
@@ -54,7 +56,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!');
|
||||
|
||||
254
lib/Actions.js
254
lib/Actions.js
@@ -4,168 +4,106 @@ 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 must be signed-in to interact with a video/channel');
|
||||
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.encodeVideoId(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_request_opts({ 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 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 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 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
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
async function notifications(session, action_type, args = {}) {
|
||||
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
|
||||
};
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'modify_channel_preference':
|
||||
let pref_types = { ALL: 0, NONE: 1, PERSONALIZED: 2 };
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeChannelId(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_request_opts({ session, data, desktop: true })).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
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { subscribe, unsubscribe, likeVideo, dislikeVideo, removeLike, commentVideo, getNotifications };
|
||||
async function getContinuation(session, info = {}) {
|
||||
let data = { context: session.context };
|
||||
|
||||
if (info.continuation_token) {
|
||||
data.continuation = info.continuation_token;
|
||||
}
|
||||
|
||||
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_request_opts({ session, data, desktop: true })).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
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { engage, notifications, getContinuation };
|
||||
@@ -161,21 +161,25 @@ const formatVideoData = (data, context, desktop) => {
|
||||
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
|
||||
// actions
|
||||
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 => {};
|
||||
// Actions
|
||||
video_details.like = () => {};
|
||||
video_details.dislike = () => {};
|
||||
video_details.removeLike = () => {};
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
if (metadata.is_live_content) {
|
||||
video_details.getLivechat = () => {};
|
||||
}
|
||||
|
||||
// additional metadata
|
||||
// Additional metadata
|
||||
video_details.metadata = metadata;
|
||||
}
|
||||
return video_details;
|
||||
};
|
||||
|
||||
const filters = (order) => {
|
||||
const filters = (order) => { // TODO: Refactor this crazy thing
|
||||
switch (order) {
|
||||
case 'any,any,relevance':
|
||||
return 'EgIQAQ%3D%3D';
|
||||
|
||||
@@ -6,6 +6,7 @@ const OAuth = require('./OAuth');
|
||||
const Utils = require('./Utils');
|
||||
const Player = require('./Player');
|
||||
const Actions = require('./Actions');
|
||||
const Livechat = require('./Livechat');
|
||||
const Constants = require('./Constants');
|
||||
const SigDecipher = require('./SigDecipher');
|
||||
const EventEmitter = require('events');
|
||||
@@ -13,7 +14,7 @@ const TimeToSeconds = require('time-to-seconds');
|
||||
const CancelToken = Axios.CancelToken;
|
||||
|
||||
class Innertube extends EventEmitter {
|
||||
constructor(cookie, sign_in) {
|
||||
constructor(cookie) {
|
||||
super();
|
||||
this.cookie = cookie || '';
|
||||
return this.init();
|
||||
@@ -90,6 +91,7 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
|
||||
async search(query, options = { period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
if (!query) throw new Error('No query was provided');
|
||||
if (!this.initialized) throw new Error('Missing Innertube data.');
|
||||
|
||||
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);
|
||||
@@ -134,23 +136,33 @@ 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);
|
||||
}
|
||||
|
||||
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.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: video_data.metadata.channel_id, pref: pref || 'NONE' });
|
||||
|
||||
return video_data;
|
||||
}
|
||||
|
||||
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');
|
||||
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,17 +171,22 @@ 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');
|
||||
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);
|
||||
response = await Axios.get(`${Constants.urls.YT_WATCH_PAGE}?v=${id}&t=8s&pbj=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);
|
||||
}
|
||||
@@ -178,6 +195,8 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
|
||||
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';
|
||||
@@ -286,7 +305,7 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(stream, true);
|
||||
response.data.pipe(stream, { end: true });
|
||||
} else {
|
||||
const chunk_size = 1048576 * 10; // 10MB
|
||||
|
||||
@@ -310,6 +329,14 @@ 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('error', (err) => {
|
||||
if (cancelled) {
|
||||
stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' });
|
||||
} else {
|
||||
stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' });
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
chunk_start = chunk_end + 1;
|
||||
chunk_end += chunk_size;
|
||||
@@ -321,14 +348,6 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('error', (err) => {
|
||||
if (cancelled) {
|
||||
stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' });
|
||||
} else {
|
||||
stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' });
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(stream, { end: false });
|
||||
};
|
||||
downloadChunk();
|
||||
@@ -339,6 +358,7 @@ class Innertube extends EventEmitter {
|
||||
cancelled = true;
|
||||
cancel();
|
||||
};
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
153
lib/Livechat.js
Normal file
153
lib/Livechat.js
Normal file
@@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
const EventEmitter = require('events');
|
||||
const Uuid = require("uuid");
|
||||
|
||||
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 = 0;
|
||||
this.running = true;
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
async sendMessage(text) {
|
||||
let data = {
|
||||
context: this.session.context,
|
||||
params: Utils.encodeChannelIdWithVideoId(this.channel_id, this.video_id),
|
||||
clientMessageId: `INntLiB${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text }]
|
||||
}
|
||||
};
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/send_message${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.response.data.error.message };
|
||||
|
||||
const deleteMessage = async () => {
|
||||
/*
|
||||
* The first request is made to get the chat options and the delete command endpoint,
|
||||
* these options contain the required params to delete a message (a string composed of clientId, the channelId of the channel you're watching, your public channelId and the id of the message you sent).
|
||||
* All put together with some binary data and then base64ed twice (yes, twice lm*o top notch security).
|
||||
**/
|
||||
const item_menu_res = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_item_context_menu?params=${response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params}&pbj=1${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify({ context: this.session.context }), Constants.innertube_request_opts({ session: this.session, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (item_menu_res instanceof Error) return { success: false, status_code: item_menu_res.response.status, message: item_menu_res.response.data.error.message };
|
||||
const chat_item_menu = item_menu_res.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
const delete_message_reqbody = {
|
||||
context: this.session.context,
|
||||
params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params
|
||||
};
|
||||
|
||||
const delete_message_cmd = await Axios.post(`${Constants.urls.YT_BASE_URL}${chat_item_menu.menuServiceItemRenderer.serviceEndpoint.commandMetadata.webCommandMetadata.apiUrl}${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify(delete_message_reqbody), Constants.innertube_request_opts({ session: this.session, delete_message_reqbody, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (delete_message_cmd instanceof Error) return { success: false, status_code: delete_message_cmd.response.status, message: delete_message_cmd.response.data.error.message };
|
||||
return { success: true, status_code: response.status };
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
deleteMessage: () => deleteMessage(),
|
||||
message_data: {
|
||||
text: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '),
|
||||
author: {
|
||||
name: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/',
|
||||
channel_id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId,
|
||||
profile_picture: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails
|
||||
},
|
||||
timestamp: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec,
|
||||
id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enqueueActionGroup(group) {
|
||||
group.forEach((action) => {
|
||||
if (!action.addChatItemAction) return;
|
||||
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_request_opts({ 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 livechat js player.
|
||||
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_request_opts({ 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
|
||||
}
|
||||
});
|
||||
|
||||
// How long we should wait to poll the chat again.
|
||||
if (continuation_contents.liveChatContinuation.continuations[0].timedContinuationData) {
|
||||
this.poll_intervals_ms = continuation_contents.liveChatContinuation.continuations[0].timedContinuationData.timeoutMs;
|
||||
} else {
|
||||
this.poll_intervals_ms = 4000;
|
||||
}
|
||||
|
||||
await this.poll();
|
||||
this.livechat_poller = setTimeout(() => this.poll(), this.poll_intervals_ms);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
clearTimeout(this.livechat_poller);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Livechat;
|
||||
78
lib/OAuth.js
78
lib/OAuth.js
@@ -7,49 +7,49 @@ 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`;
|
||||
|
||||
|
||||
// Used to check whether an access token is valid or not.
|
||||
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;
|
||||
|
||||
|
||||
// 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>.+?)\"/;
|
||||
|
||||
|
||||
if (creds.access_token != undefined && creds.refresh_token != undefined) return;
|
||||
this.requestAuthCode();
|
||||
}
|
||||
|
||||
async waitForAuth(device_code) {
|
||||
|
||||
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)
|
||||
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 +73,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 +83,86 @@ class OAuth extends EventEmitter {
|
||||
}
|
||||
}, 1000 * this.refresh_interval);
|
||||
}
|
||||
|
||||
|
||||
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(),
|
||||
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)
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get auth code.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
|
||||
this.emit('auth', {
|
||||
code: response.data.user_code,
|
||||
code: response.data.user_code,
|
||||
status: 'AUTHORIZATION_PENDING',
|
||||
expires_in: response.data.expires_in,
|
||||
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;
|
||||
|
||||
const identity_function = Utils.getStringBetweenStrings(response.data, '=function(){var a=window.environment', '(function()');
|
||||
const client_identity = identity_function.match(this.identity_regex).groups;
|
||||
return client_identity;
|
||||
}
|
||||
|
||||
async refreshAccessToken (refresh_token) {
|
||||
|
||||
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)
|
||||
if (response instanceof Error)
|
||||
return this.emit('refresh-token', {
|
||||
error: 'Could not refresh 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;
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ 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);
|
||||
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);
|
||||
|
||||
fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
|
||||
fs.writeFileSync(this.tmp_cache_dir + '/' + this.player_name + '.js', response.data);
|
||||
fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data);
|
||||
|
||||
this.getSigDecipherCode(response.data);
|
||||
this.getNEncoder(response.data);
|
||||
@@ -30,9 +30,9 @@ 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) {
|
||||
|
||||
31
lib/Utils.js
31
lib/Utils.js
@@ -39,8 +39,33 @@ function createFunction(input, raw_code) { // I hate this
|
||||
return new Function(input, raw_code);
|
||||
}
|
||||
|
||||
function encodeId(id) {
|
||||
return encodeURI(new Buffer.from(`` + id + `*`).toString('base64').replace('==', '') + 'BQBw==');
|
||||
function encodeVideoId(id) {
|
||||
return encodeURIComponent(`${Buffer.from(`` + id + `*`).toString('base64').slice(0, -1)}BQBw==`);
|
||||
}
|
||||
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeId };
|
||||
function encodeChannelId(id, notification_pref) {
|
||||
const buff_start = `
|
||||
`;
|
||||
const buff_end = [
|
||||
``, // all
|
||||
``, // none
|
||||
``, // personalized
|
||||
];
|
||||
|
||||
let encodedId = Buffer.from([buff_start, id, buff_end[notification_pref]].join('')).toString('base64');
|
||||
return encodeURIComponent(`${encodedId}GAAgBA==`);
|
||||
}
|
||||
|
||||
function encodeChannelIdWithVideoId(channel_id, video_id) {
|
||||
const buff_start = `
|
||||
)*'
|
||||
`;
|
||||
const buff_middle = ``;
|
||||
const buff_end = ``;
|
||||
|
||||
// Yes, we also have to base64 these twice lol
|
||||
let encodedIds = Buffer.from([buff_start, channel_id, buff_middle, video_id, buff_end].join('')).toString('base64');
|
||||
return `${Buffer.from(encodedIds).toString('base64').slice(0, -4)}JTNE`;
|
||||
}
|
||||
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeChannelIdWithVideoId, encodeVideoId, encodeChannelId };
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
@@ -99,9 +99,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.805",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.805.tgz",
|
||||
"integrity": "sha512-asL9HOjdiJw+A7T7rYnxFC3wAt3u7Shle8fWHECKoqFV2JqI21Xoh8i29cdIsWP1Tn8FZcB0doUQ0h1pUwLkxw==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -174,9 +174,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.805",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.805.tgz",
|
||||
"integrity": "sha512-asL9HOjdiJw+A7T7rYnxFC3wAt3u7Shle8fWHECKoqFV2JqI21Xoh8i29cdIsWP1Tn8FZcB0doUQ0h1pUwLkxw==",
|
||||
"requires": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
@@ -27,6 +27,7 @@
|
||||
"youtube-dl",
|
||||
"innertube",
|
||||
"innertubeapi",
|
||||
"livechat",
|
||||
"api",
|
||||
"search",
|
||||
"like",
|
||||
|
||||
Reference in New Issue
Block a user